diff --git a/README_PACKAGERS.md b/README_PACKAGERS.md index 7cd8faa7e1..c688d58de7 100644 --- a/README_PACKAGERS.md +++ b/README_PACKAGERS.md @@ -1,71 +1,71 @@ = Notes for Packagers = == Patching Qt == Qt 5.6 is currently the recommended version to build Krita with on all platforms. However, Qt 5.6 on Linux needs to be patched for https://bugreports.qt.io/browse/QTBUG-44964 . The patch in 3rdparty/ext_qt/qt-no-motion-compression.diff == Package Contents == We recommend that all of Krita packaged in one package: there is no need to split Krita up. In particular, do not make a separate package out of the plugins directory; without the plugins Krita will not even start. Krita does not install header files, so there is no need for a corresponding -dev(el) package. == Third Party Libraries == The top-level 3rd-party directory is not relevant for packaging: it only contains CMake projects for all of Krita's dependencies which are used for building Krita on Windows and OSX. It is not called from the top-level CMakeLists.txt project. There are four forks of 3rd party libraries that are relevant and cannot be replaced by system libraries: -* plugins/impex/raw/3rdparty contains a fork of kdcraw. Upstread removed most functionality from this library and is in general unable to provide a stable API. The library has been renamed to avoid conflicts with upstream kdcraw. +* plugins/impex/raw/3rdparty contains a fork of kdcraw. Upstream removed most functionality from this library and is in general unable to provide a stable API. The library has been renamed to avoid conflicts with upstream kdcraw. * plugins/impex/xcf/3rdparty contains the xcftools code. This has never been released as a library * libs/image/3rdparty contains einspline. This code is directly linked into the kritaimage library and has never been released as a separate library. == Build flags == Krita no longer supports a build without OpenGL. For alpha and beta packages, please build with debug output enabled, but for production packages the -DCMAKE_CXX_FLAGS="-DKDE_NO_DEBUG_OUTPUT" is recommended. A significant performance increase will be the result. If you build Krita with RelWithDebInfo to be able to create a corresponding -dbg package, please define -DQT_NO_DEBUG=1 as well to disable asserts. == Dependencies == Krita depends on: * boost and the boost-system library * eigen3 * exiv2 * fftw3 * gsl * ilmbase * jpeg: Note that libjpeg-turbo is recommended. * lcms2 * libraw * opencolorio * openexr * png * poppler-qt5 * pthreads * qt-5: Note that Qt 5.6 is _strongly_ recommended. Qt 5.5 has bugs that interfere with proper handling of tablet events * tiff * vc: this is a build-time dependency only * zlib And the following KDE Frameworks: * Archive * Completion * Config * CoreAddons * GuiAddons * I18n * ItemModels * ItemViews * KCrash * WidgetsAddons * WindowSystem diff --git a/krita/doc/strokes/strokes_documentation.org b/krita/doc/strokes/strokes_documentation.org index 5e0d7480c6..2c3a5917a3 100644 --- a/krita/doc/strokes/strokes_documentation.org +++ b/krita/doc/strokes/strokes_documentation.org @@ -1,705 +1,705 @@ #+TITLE: Strokes Documentation #+AUTHOR: Dmitry Kazakov #+EMAIL: dimula73@gmail.com * Strokes queue ** Strokes, jobs... What it is all about? (theory) *** Structure of a stroke An abstraction of a /stroke/ represents a complete action performed by a user. This action can be canceled when it has not been finished yet, or can be undone after it's undo data has been added to the undo stack. Every stroke consists of a set of /stroke jobs/. Every job sits in a queue and does a part of work that the stroke as a whole must perform on an image. A stroke job cannot be canceled while execution and you cannot undo a single job of the stroke without canceling the whole stroke. *Example:* Lets look at how the Freehand Tool works. Every time the user paints a single line on a canvas it creates a /stroke/. This stroke consists of several /stroke jobs/: one job initializes indirect painting device and starts a transaction, several jobs paint dabs of a canvas and the last job merges indirect painting device into the canvas and commit the undo information. The jobs of the stroke can demand special order of their execution. That is the way how they will be executed on a multi-core machine. Every job can be either of the type: - =CONCURRENT= :: /concurrent/ job may be executed in parallel with any other concurrent job of the stroke as well as with any update job executed by the scheduler *Example:* in Scale Image action each job scales its own layer. All the jobs are executed in parallel. - =SEQUENTIAL= :: if the job is /sequential/, no other job may interleave with this one. It means that when the scheduler encounters a sequential job, it waits until all the other stroke jobs are done, starts the sequential job and will not start any other job until this job is finished. Note that a sequential job can be executed in parallel with update jobs those merge layers and masks. *Example:* All the jobs of the Freehand Tool are sequential because you cannot rearrange the painting of dabs. And more than that, you cannot mix the creation of the transaction with painting of anything on a canvas. - =BARRIER= :: /barrier/ jobs are special. They created to allow stroke jobs to synchronize with updates when needed. A barrier job works like a sequential one: it does not allow two stroke jobs to be executed simultaneously, but it has one significant addition. A barrier job will not start its execution until /all/ the updates (those were requested with =setDirty()= calls before) has finished their execution. Such behavior is really useful for the case when you need to perform some action after the changes you requested in previous jobs are done and the projection of the image does now correspond the changes you've just done. *Example:* in Scale Image action the signals of the image like =sigSizeChanged= should be emitted after all the work is done and all the updates are finished. So it runs as a barrier job. See =KisProcessingApplicator= class for details. Besides one of the types above a job may be defined as =EXCLUSIVE=. Exclusive property makes the job to be executed on the scheduler exclusively. It means that there will be no other jobs (strokes or updates) executed in parallel to this one. *** The queue of strokes The strokes themselves are stored in a queue and executed one by one. This is important to know that any two jobs owned by different strokes cannot be executed simultaneously. That is the first job of a stroke starts its execution only /after/ the last job of the previous stroke has finished. The stroke is just a container for jobs. It stores some information about the work done, like =id()= and =name()=. Alongside storing this information it can affect the order of execution of jobs as well. The stroke can be defined /exclusive/. The meaning of this resembles the behavior of stroke job's exclusive property. /Exclusive stroke/ is a stroke that executes its jobs with all the updates blocked. The execution of updates will start only after the stroke is finished. ** Implementation (practice) *** Implementation of a stroke #+CAPTION: Overview of stroke classes [[./img/strokes_queue_internals.png]] Each stroke is represented by a =KisStroke= object. It has all the basic manipulating methods like: =addJob()=, =endStroke()= and =cancelStroke()=. The behavior of a stroke is defined by a /stroke strategy/ (KisStrokeStrategy class). This strategy is passed to the KisStroke object during construction and owned by the stroke. Each stroke job is represented by =KisStrokeJob= object. The queue of =KisStrokeJob= objects is stored in every stroke object. This very object is used for actual running the job (=KisUpdateJobItem= calls =KisStrokeJob::run()= method while running). The behavior of the stroke job is defined by a strategy (=KisStrokeStrategy=) and a data (=KisStrokeJobData=). Those two objects are passed during the construction of the KisStrokeJob object. A stroke can have four types of jobs: - initialization - canceling - finishing - actual painting (named as 'dab' in the code) During construction the stroke asks its strategy to create strategies for all the four types of job. Then it uses these strategies on creation of jobs on corresponding events: initialization, canceling, finishing and when the user calls =addJob()= method. The strategies define all the properties of strokes and stroke jobs we were talking above. The data class is used for passing information to the stroke by high-level code. *Example:* =FreehandStrokeStrategy::Data= accepts such information as: =node=, =painter=, =paintInformation=, =dragDistance= Other information that is common to the whole stroke like names of the paintOp, compositeOp are passed directly to the constructor of the stroke strategy. *** Execution of strokes by =KisStrokesQueue= The key class of the strokes' execution is =KisStrokesQueue=. The most important method that is responsible for applying all the rules about interleaving of jobs mentioned above is =KisStrokesQueue::processOneJob=. This method is called by the update scheduler each time a free thread appears. First it gets the number of merge and stroke jobs currently executing in the updater context. Then it checks all the rules one by one. *** Canceling and undo information trick It was stated above that a stroke can be canceled in each moment of time. That happens when a user calls =KisStroke::cancelStroke()= method. When it is requested the stroke drops all the jobs those are present in its queue and has not been started yet. Then it enqueues a special kind of job named /cancel job/ that reverts all the work done by the stroke. This is used for interactive canceling of tools' strokes. Taking into account that the strokes can be reverted, we cannot use =QUndoStack= capabilities directly. We should add commands to the stack /after/ they have been executed. This resembles the way how =KisTransactionData= works: its first redo() method doesn't do anything because everything has already been painted on a device. Here in strokes this "after-effect-addition" is implemented in general way. Strokes work with a special kind of undo adapter: =KisPostExecutionUndoAdapter=. This adapter wraps the commands in a special wrapper that puts them into the stack without calling =redo()= and controls their threaded =undo()= and =redo()= operations. See information about =KisPostExecutionUndoAdapter= in a separate document. *** Queues balancing So we ended up with a solution where our scheduler has two queues that it should spread between limited amount of threads. Of course there should be some algorithm that balances the queues. Ideally, we should balance them by the total area of image the queue should process. But we cannot achieve that currently. So the formula for size metrics is quite simple: ~updatesMetric = ~ ~strokesMetric = * ~ Balancing formula: ~balancingRatio = / ~ *** Starting a stroke The main entry point to strokes for the user is =KisStrokesFacade= interface. This interfaces provides four methods: =startStroke()=, =addJob()=, =endStroke()= and =cancelStroke()=. So every time you work with strokes you should work using this interface. *Note:* KisImage and KisUpdateScheduler both implement this interface, so you can use them as a strokes facade. But please try not to store pointers to the whole image. Try store a link to interface only, if possible. So if you want to start a stroke you should do the following: 1) Create a stroke strategy 2) Start a stroke with: =KisStrokeId strokeId = strokesFacade->startStroke(myStrategy);= *Note:* you'll get a KisStrokeId handle for the stroke you created. This handle will be used in all the other methods for controlling the stroke. This handle is introduced, because several users can access the strokes facade simultaneously, so there may be several strokes opened simultaneously. It's important to understand that even when several strokes are opened simultaneously, only one of them executes on the cpu. All the other strokes will be delayed until it is finished. 3) Create a data for your stroke job 4) Add a job to the execution queue: =strokesFacade->addJob(strokeId, myData);= 5) You may add as many jobs as you wish 6) End or cancel the stroke: =strokesFacade->endStroke(strokeId);= or =strokesFacade->cancelStroke(strokeId);= * Strokes public API ** Simplified stroke classes As you might noticed the internal strokes API is quite complex. If you decide to create your own stroke you need to create at least six new classes: - stroke strategy class - four stroke jobs strategies (init, finish, cancel, dab) - data that will be passes to a dab-strategy-based job That is not really a good solution for a public API, so we introduced an adapter that simplifies all these stuff. The class is called =KisSimpleStrokeStrategy=. It allows you to define all the jobs you need in a single class. #+CAPTION: Simple stroke classes [[./img/strokes_simplified_api.png]] This class has four virtual methods those you can use as callbacks. When you need to use one of them just override it in your own class and add activation of the corresponding callback to the constructor of your class: #+BEGIN_SRC c++ class MyOwnStroke : public KisSimpleStrokeStrategy { MyOwnStroke() { enableJob(KisSimpleStrokeStrategy::JOB_INIT); enableJob(KisSimpleStrokeStrategy::JOB_FINISH); enableJob(KisSimpleStrokeStrategy::JOB_CANCEL); enableJob(KisSimpleStrokeStrategy::JOB_DAB); } void initStrokeCallback() { } void finishStrokeCallback() { } void cancelStrokeCallback() { } void doStrokeCallback(KisStrokeJobData *data) { Q_UNUSED(data); } }; #+END_SRC Internally, =KisSimpleStrokeStrategy= creates all the job strategies needed for the lowlevel API. And these internal job strategies call the callbacks of the parental class. *Important:* Notice that the job data passed to /init/, /finish/ and /cancel/ jobs is always null. It means that these jobs will always be /sequential/ and /non-exclusive/. That is done intentionally to simplify the API. At the same time that is a limitation of the API. But currently, this is perfectly enough for us. ** Unit-testing of the strokes One of the benefits of using the strokes is that you are able to test them separately from the UI using a common infrastructure. *** =utils::StrokeTester= class That is a really simple class that you can use to test your own stroke. It test the following aspects of your stroke: - canceling of the stroke - working with indirect painting activated - testing updates of the image projection after your stroke - working with a layer that is not connected to any image The result of the execution is compared against the reference png files those you create manually while writing your test. *** How to write your own test You can check examples in =MoveStrokeTest= and =FreehandStrokeTest= tests. 1) You need to inherit your tester class from =utils::StrokeTester=. The constructor of that class accepts the name of your stroke (it'll be used for generating filenames), size of the image and a filename of the preset for the paintOp. #+BEGIN_SRC c++ StrokeTester(const QString &name, const QSize &imageSize, const QString &presetFileName = "autobrush_300px.kpp"); #+END_SRC 2) Then you need to override at least two methods: #+BEGIN_SRC c++ KisStrokeStrategy* createStroke(bool indirectPainting, KisResourcesSnapshotSP resources, KisPainter *painter, KisImageWSP image); void addPaintingJobs(KisImageWSP image, KisResourcesSnapshotSP resources, KisPainter *painter); #+END_SRC If you thing you need it you may do some corrections for the image and active node in the following method: #+BEGIN_SRC c++ void initImage(KisImageWSP image, KisNodeSP activeNode); #+END_SRC 3) Run your test in a testing slot: #+BEGIN_SRC c++ void MyStrokeTest::testStroke() { MyTester tester(); tester.test(); } #+END_SRC 4) During the first run the test will report you many fails and will generate you several files with actual result of the test. You need to check these files, then move them into the tests' data folder: =tests/data//= 5) After you copied the files the tester will compare the actual result against these very files. That means it'll catch all the changes in the work of your stroke, so you'll be able to catch all the regressions automatically. ** Predefined classes for usage as base classes *** =KisPainterBasedStrokeStrategy= This class can be used for the strokes those work with the node using a painter (or painters like in =KisToolMultihand=). This class accepts resources snapshot (=KisResourcesSnapshot=) and a painter (painters). Initialization, finishing and canceling callbacks of this class do all the work for dealing with indirect painting support, creation of transaction, reverting the stroke on canceling. This base class is used for =FreehandStroke= mostly. *** =KisStrokeStrategyUndoCommandBased= It is obvious from the name of the class that it works with undo commands. In constructor you define which method of undo command should be used undo() or redo(). Afterwards, you just add commands to the stroke and they are executed with any the sequentiality constraints. This stroke strategy does all the work for adding the commands to the undo adapter and for canceling them if needed. ** Example classes - =KisPainterBasedStrokeStrategy= - =FreehandStrokeStrategy= - =KisStrokeStrategyUndoCommandBased= - =MoveStrokeStrategy= * Internals of the freehand tool #+CAPTION: Freehand tool classes [[./img/freehand_tool_internals.png]] ** Motivation for so many classes We need to share the codebase between at least four classes: =KisToolFreehand=, =KisToolMultihand=, =KisScratchPad=. All these classes paint on a canvas with =KisPainter=, so they share quite much common code. ** KisResourcesSnapshot After we introduced the strokes, the moments of time when user paints with mouse and when the line is actually painted on the canvas do not coincide. It means that by the time a thread starts actual changing the device, the contents of =KoCanvasResourceProvider= might have already changed. So before we start a stroke we should create a snapshot of all the resources we have and pass this snapshot to the stroke. For this purpose we introduced =KisResourcesSnapshot= class. It solves two problems at the same time: first it stores all the resources we might have and second it encapsulates the algorithm of loading these resources into a =KisPainter= object. So this class is really easy to use. You just create the snapshot and then just load all the resources to the painter when needed. #+BEGIN_SRC c++ KisResourcesSnapshotSP resources = new KisResourcesSnapshot(image, undoAdapter, resourceManager); KisPainter painter; painter.begin(device, selection); resources->setupPainter(&painter); // paint something painter.end(); #+END_SRC In our implementation this class is usually created by =KisToolFreehandHelper= and passed to the =KisPainterBasedStrokeStrategy= class. The latter one creates painters and initializes them using =setupPainter()=. ** =KisToolFreehand= and =KisScratchPad= The freehand tool is split into four classes: - =KisToolFreehand= :: highlevel tool class that get the mouse events form the Ko-classes and distributes events among internal classes. - =KisToolPaintingInformationBuilder= :: converts mouse events represented by =KoPointerEvent= objects into =KisPaintInformation= objects. - =KisRecordingAdapter= :: stays in charge of adding recording information into the image's action recorder. This class has two purposes: first we need to be able to disable recording for the scratch pad (then we just pass NULL instead of a recording adapter), second when the strokes are able to do their own recording, it'll be easier to port the freehand tool to it. - =KisToolFreehandHelper= :: this is the main class that combines all the classes we were talking above. It accepts a mouse event, converts it using a painting information builder into the paint information, notifies recording adapter, takes the snapshot of resources and finally starts a stroke. Then it populates the stroke with stroke jobs, when the user moves the mouse (=paint(event)= method) and finishes the stroke in the end. Such splitting allows us to use the same classes in both =KisToolFreehand= and =KisScratchPad=. The only difference between them is that the scratch pad doesn't have a recording adapter at all, and uses base class =KisPaintingInformationBuilder= instead of =KisToolPaintingInformationBuilder=. The latter differs from the former one in a way that it supports painting assistants (=adjustDocumentPoint()= method), complex coordinate transformations with =KisCoordinatesConverter= (=documentToImage()= method) and perspective painting (=calculatePerspective()= method). The rest of the code is shared. ** =KisToolMultihand= Multihand tool uses the same classes. The only difference, it has a couple of modifications in its helper (=KisToolMultihandHelper=), those allow it to have several painters at the same time. The tool's class inherits the freehand tool's class and just substitutes the helper with its own (with =resetHelper()= method). * Scheduled Undo/Redo ** Two ways of working with undo commands The key problem of designing the undo system for strokes was that there are two ways of working with undo commands. That is we have two types of commands actually: - /Qt-like command/ - command's redo() method is executed while the command is added into the undo stack - /Transaction-like command/ - the command is added to the stack /after/ its action has already been performed. It means that the first redo() of this command (the one that is called by undo stack) does nothing. That is a transaction-like command just saves undo data for the future and does not perform anything on addition. You already know that our strokes can be reverted on the go, it means that the stroke's undo command should be added to the undo stack only /after/ all the actions of the stroke have been performed. So it looks like the stroke's commands are /transaction-like/. But there is another problem: the stroke should be able to execute regular undo commands those are not transaction-like (like is it done in =KisStrokeStrategyUndoCommand=). More than that, undo and redo of for such strokes should be performed with the same sequentiality properties (read "undo/redo operations should be threaded as well"). It follows that the undo commands generated by the stroke should be wrapped in a special /wrapper command/, lets call it =KisSavedCommand=, that hold the following properties: - the wrapper skips the first redo(). It means the wrapped command's redo() method will not be called on its addition to the stack. Obviously, it is not needed, because the action has already been performed by the stroke itself. - when undo stack calls to undo/redo methods of the wrapper-command, the command creates a stroke (=KisStrokeStrategyUndoCommandBased=) and runs the wrapped command in a context of this stroke. - a special /macro wrapper command/, lets call is =KisSavedMacroCommand=, should be able to save all the commands executed by a stroke and undo/redo all of them in the original order with original sequentiality properties (concurrent, sequential, barrier, exclusive). That is exactly what we have: =KisSavedUndoCommand= skips the first redo and runs undo()/redo() of an internal command in a separate stroke. We have =KisSavedMacroCommand= as well to save the contents of the whole stroke. #+CAPTION: Scheduled commands [[./img/scheduled_undo_redo.png]] ** New Undo Adapters Well, it would be quite insane to ask all the users of strokes to wrap their commands into wrapper, so we introduced a separate undo adapter for strokes: =KisPostExecutionUndoAdapter=. This adapter wraps your command and puts it into the undo stack automatically. This is the only adapter we can use inside strokes, that is why all the strokes accept the pointer to it. For the legacy code we still have =KisUndoAdapter=, but now we call it "legacy undo adapter". It works as usual: it adds a command to undo stack directly, so it gets executed right in the moment of addition. But there still is one trick. Stroke's commands come to the undo stack asynchronously, so if we try to simply add a command to the stack, we can catch a race condition easily. That's why the legacy undo adapter must guard itself from strokes with locking the strokes system. That is done with a special kind of lock =barrierLock()=. This barrier lock differs from a regular lock in a way that it ways for all the running /strokes/ are finished, while a regular lock waits for all the running /stroke jobs/ are done. That's the only difference. The same race conditions problem applies to the undo()/redo() signals from the UI. The user may request the undo operation while the stroke is adding its commands. This will surely lead to a crash. We solved this problem in a bit hacky way: we hacked =QUndoStack= and made it's undo()/redo() slots virtual. After that we overridden the stack with our own, and changed these methods to block the strokes while undo()/redo() is happening. We use =tryBarrierLock()= there, because it is easier to cancel the undo than to wait until all the strokes are finished. ** Undo Adapters and Undo Stores Well, we have two types of undo adapters now (not counting =KisSurrrogateUndoAdapter=). It's obvious that they should share some code. That is why we split the work with the actual undo stack into a separate class =KisUndoStore=. So now the undo store defines "where to store the undo data", and undo adapter defines "how to adapt krita's commands to qt's stack". There are additional types of store classes for using in tests and for special purposes. #+CAPTION: Undo Adapter vs Undo Store [[./img/undo_adapters.png]] * Processings framework ** Motivation In Krita we have many actions which have common structure of execution. Take a look at actions like Scale Image, Rotate Image, Change Color Space - all of them have common phases: 1) Lock the image 2) Do the processing of nodes 3) Unlock the image 4) Emit setDirty() calls and update the projection of the nodes 5) Wait until all the setDirty()'es are finished 6) Emit image's signals like sigImageSizeChanged More than that, you should pay attention to the fact that all these actions should support undo/redo operations. And the last two phases cannot be implemented as usual qt-commands inside a usual macro, because they should always be executed /in the end/ of the action (in qt commands are executed in reverse order during undo operations, that is not what we want). And, btw, it would be really good idea to have multithreading support for such actions, because some of them (like Scale Image) may be quite slow. =KisNodeVisitor= cannot fit all these requirements, because it has important design limitations: first, walking through nodes is implemented inside the visitor itself and, second, emitting signals is put into visitors as well. These two limitations prevent the code to be shared between actions. That is why we introduced new shiny =KisProcessingVisitor= and a separate framework for them. ** Processing visitors #+CAPTION: Processing framework [[./img/processings_framework.png]] The key class of the processing framework is =KisProcessingVisitor=. Its main difference from the old visitor is that it is extremely simple. It performs one task only, it processes one node. And that is all. It does no locking, performs no updates, emits no signals. It just processes (that is, changes the content) a single node. You can look at the reference implementation of it in =KisCropProcessingVisitor= and =KisTransformProcessingVisitor=. The key idea of this framework is to keep the processings as simple as possible. So the rest of the work is done by external classes, those are shared between all the processings. We have one such class. Its name is =KisProcessingApplicator=. This class performs several tasks: - creates a stroke. So all the actions executed with this applicator will be undo/redo'able. - applies a visitor to a requested node. - applies a visitor recursively to a node and all its children. Note, that you can choose any sequentiality property for the execution of your visitor. It means that the visitors can be applied to nodes concurrently in multithreaded way. - applies a usual qt-command to the image. Sequentiality properties may vary as well. - emits setDirty() calls for all the nodes which need it. It is done in efficient way, so no nodes are updated twice. - emits image signals /after/ all the actions and updates are finished. Lets look at an example: #+BEGIN_SRC c++ void KisImage::resizeImageImpl(const QRect& newRect, bool cropLayers) { if(newRect == bounds()) return; QString actionName = cropLayers ? i18n("Crop Image") : i18n("Resize Image"); (1) KisImageSignalVector emitSignals; (2) emitSignals << SizeChangedSignal << ModifiedSignal; (3) KisProcessingApplicator applicator(this, m_d->rootLayer, KisProcessingApplicator::RECURSIVE, emitSignals, actionName); if(cropLayers || !newRect.topLeft().isNull()) { (4) KisProcessingVisitorSP visitor = new KisCropProcessingVisitor(newRect, cropLayers, true); (5) applicator.applyVisitor(visitor, KisStrokeJobData::CONCURRENT); } (6) applicator.applyCommand(new KisImageResizeCommand(this, newRect.size())); (7) applicator.end(); } #+END_SRC In lines (1) and (2) we create a list of signals we should emit after the execution of the applicator. This list should be passed to the /constructor/ of the applicator (3) (the list is passed to the constructor instead of end() function, because we face a limitation connected with the internals of the implementation of undo for processings, I doubt it can create any troubles). In the line (3) we create a recursive applicator. In lines (4) and (5) we create a visitor and apply it to nodes recursively in a multithreaded way. *Warning:* the visitor is shared between all the threads so it should be written in a /thread-safe/ way. In line (6) we apply a command sequentially, it means that it'll be executed right after /all/ the threads with visitors has finished. Line (7) closes the stroke an tells it to perform all the updates and emit all the signals. ** Implementation of =KisProcessingApplicator= The applicator is based on the "undo command"-based stroke (=KisStrokeStrategyUndoCommandBased=). It starts the stroke in the constructor and adds undo commands to it on every user request. The processings are inernally wrapped into a special command (=KisProcessingCommand=). This command has its own undo stack that collects the transactions executed by the processing. This can be easily achieved with our undo adapters interface. The command just defines its own =KisSurrogateUndoAdapter= and passes it to the processing. Processing adds its transactions to the fake adapter. And later, the command just uses the undo stack to undo/redo actions executed by the transaction. The applicator defines several internal commands as well: =UpdateCommand= and =EmitSignalsCommand=. These commands are added to the beginning and to the end of every stroke, so that they can be executed in the end of both undo and redo operations. The parameter =finalUpdate= controls whether the command is executed during its redo() or undo() operation. ** Emission of signals trick After actions have been moved to separate threads, problems with image signals appeared. When everything was executed in a single thread the connection of signals like =sigAboutToAddNode= and =sigNodeHasBeenAdded= worked as /Qt::DirectConnection/. So these signals were effectively function calls. After we moved the actions to a separate thread, all of them became /Qt::QueuedConnection/. I guess you know what it means. They simply lost all their sense. So we had to start to use /Qt::BlockingQueuedConnection/. But there is another problem with it. Some of the (old) code is still executed in a context of the UI thread and they emit signals as well. So all that code causes deadlocks when using =Qt::BlockingQueuedConnection=. That is why we had to introduce =KisImageSignalRouter=. This class checks which thread emits the signal and emits it either using =Qt::DirectConnection= or =Qt::BlockingQueuedConnection=. So no deadlocks are possible. ** Progress reporting The fact that a processing visitor does a really simple task (processes a single node) that is very easy to report progress using progress bars in the layer box. We just need to use progress pxoxy of the node we process (=KisNodeProgressProxy=). Our processings framework provides even easier way of doing this. You just need to instantiate a =ProgressHelper= object and ask it to - greate a =KoUpdater= object for you. And all is done. You can see + create a =KoUpdater= object for you. And all is done. You can see an example in =KisTransformProcessingVisitor= class. ** Testing Usage of a common framework makes testing really simple. There is a separate unittest in image's tests folder: =KisProcessingsTest=. To test a processing you need to write just a couple of lines. Everything is done by =BaseProcessingTest= helper class. This class will run your processing and compare results against reference png files those are stored in data folder. If there are some problems found, it'll dump result files to the current directory. diff --git a/libs/color/colord/org.freedesktop.ColorManager.Device.xml b/libs/color/colord/org.freedesktop.ColorManager.Device.xml index 390f7172f2..9cec83f0ac 100644 --- a/libs/color/colord/org.freedesktop.ColorManager.Device.xml +++ b/libs/color/colord/org.freedesktop.ColorManager.Device.xml @@ -1,510 +1,510 @@ The interface used for querying color parameters for a specific device. The date the device was created. The date the device was last modified, which in this instance means having a profile added or removed, or a different profile set as default. The device model string. The device serial string. The device vendor string. The device supported colorspace string, e.g. RGB. The device kind, e.g. scanner, display, printer or camera The device id string. The profile paths associated with this device. Profiles are returned even if the device is disabled or is profiling, and clients should not assume that the first profile in this array should be applied. The mode of the device, e.g. virtual, physical or unknown. Virtual devices are not tied to a specific item of hardware and can represent abstract devices such as "Boots Photo Lab". Physical devices correspond to a connected device that cannot be removed by client software. If a virtual 'disk' device gets added by a client then it is promoted to a 'physical' device. This can happen if a printer is saved and then restored at next boot before the CUPS daemon is running. The qualifier format for the device, e.g. ColorModel.OutputMode.OutputResolution. The scope of the device, e.g. normal, temp or disk. The user ID of the account that created the device. If the device is enabled. Devices are enabled by default until Device.SetEnabled(False) is called. If the enabled state is changed then this is reflected for all users and persistent across reboots. The seat that the device belongs to, or an empty string for none or unknown. If the device is embedded into the hardware itself, for example the internal webcam or laptop screen. The metadata for the device, which may include optional keys like XRANDR_name. The bus names of all the clients that have inhibited the device for profiling. e.g. [ ":1.99", ":1.109" ] Sets a property on the object. The property name, e.g. Model. The property value, e.g. RGB.Plain.. Some value on the interface has changed. Adds a profile to the device. The profile must have been previously created. This method also stores the device to profile mapping in a persistent datadase, so that if the device and profile happen to both exist in the future, the profiles are auto-added to the device. The strength of the relationship from profile to device. This can be soft to indicate that the mapping is not important, or that the profile is assumed from a device and not in response to user action. The default option is hard, and this means that the user has explicitly mapped a profile to - a device, and this should take precidence over any + a device, and this should take precedence over any soft profiles. If the user makes a soft profile default, then it is explicitly promoted to a hard relationship. The profile path to add. Removes a profile for a device. This method also removes the device to profile mapping from a persistent datadase, so that if the device and profile happen to both exist in the future, the profiles are no longer auto-added to the device. If the profile was automatically added due to metadata in the profile (e.g. the profile was created for the device) then manually removing the profile will cause this metadata add to be suppressed. This allows the user to remove old or obsolete profiles from any color control panel without having to delete them. The profile path that has already been added to the device. Sets the default profile for a device. The profile path that has already been added to the device. Gets a single profile object path for a qualifier. The search term can contain '*' and '?' wildcards. An array of qualifiers, e.g. ['RGB.*.300dpi', 'RGB.*.*', '*.*.*']. If the qualifier * is specified then the default profile is returned. The profile path for the search terms. Gets a profile relation for a given profile that has been added to this device. A profile object path. The profile to device relation, e.g. hard. Adds an inhibit on all profiles for this device. This means that any calls to GetProfileForQualifier will always match no profiles. This method will be used when creating profiles for devices, where the session color manager wants to be very sure that no profiles are being applied wen displaying color samples or printing color swatches. If the calling program exits without calling ProfilingUninhibit then the inhibit is automatically removed. Removes an inhibit on the device. This method should be used when profiling has finished and normal device matching behaviour should resume. Sets the device enable state. If the device is enabled. diff --git a/libs/flake/KoShape.cpp b/libs/flake/KoShape.cpp index f0095ebed3..8416219de8 100644 --- a/libs/flake/KoShape.cpp +++ b/libs/flake/KoShape.cpp @@ -1,2545 +1,2545 @@ /* This file is part of the KDE project Copyright (C) 2006 C. Boemann Rasmussen Copyright (C) 2006-2010 Thomas Zander Copyright (C) 2006-2010 Thorsten Zachmann Copyright (C) 2007-2009,2011 Jan Hambrecht CopyRight (C) 2010 Boudewijn Rempt This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include "KoShape.h" #include "KoShape_p.h" #include "KoShapeContainer.h" #include "KoShapeLayer.h" #include "KoShapeContainerModel.h" #include "KoSelection.h" #include "KoPointerEvent.h" #include "KoInsets.h" #include "KoShapeStrokeModel.h" #include "KoShapeBackground.h" #include "KoColorBackground.h" #include "KoHatchBackground.h" #include "KoGradientBackground.h" #include "KoPatternBackground.h" #include "KoShapeManager.h" #include "KoShapeUserData.h" #include "KoShapeApplicationData.h" #include "KoShapeSavingContext.h" #include "KoShapeLoadingContext.h" #include "KoViewConverter.h" #include "KoShapeStroke.h" #include "KoShapeShadow.h" #include "KoClipPath.h" #include "KoPathShape.h" #include "KoOdfWorkaround.h" #include "KoFilterEffectStack.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_assert.h" #include "KoOdfGradientBackground.h" #include // KoShapePrivate KoShapePrivate::KoShapePrivate(KoShape *shape) : q_ptr(shape), size(50, 50), parent(0), shadow(0), border(0), filterEffectStack(0), transparency(0.0), zIndex(0), runThrough(0), visible(true), printable(true), geometryProtected(false), keepAspect(false), selectable(true), detectCollision(false), protectContent(false), textRunAroundSide(KoShape::BiggestRunAroundSide), textRunAroundDistanceLeft(0.0), textRunAroundDistanceTop(0.0), textRunAroundDistanceRight(0.0), textRunAroundDistanceBottom(0.0), textRunAroundThreshold(0.0), textRunAroundContour(KoShape::ContourFull) { connectors[KoConnectionPoint::TopConnectionPoint] = KoConnectionPoint::defaultConnectionPoint(KoConnectionPoint::TopConnectionPoint); connectors[KoConnectionPoint::RightConnectionPoint] = KoConnectionPoint::defaultConnectionPoint(KoConnectionPoint::RightConnectionPoint); connectors[KoConnectionPoint::BottomConnectionPoint] = KoConnectionPoint::defaultConnectionPoint(KoConnectionPoint::BottomConnectionPoint); connectors[KoConnectionPoint::LeftConnectionPoint] = KoConnectionPoint::defaultConnectionPoint(KoConnectionPoint::LeftConnectionPoint); connectors[KoConnectionPoint::FirstCustomConnectionPoint] = KoConnectionPoint(QPointF(0.5, 0.5), KoConnectionPoint::AllDirections, KoConnectionPoint::AlignCenter); } KoShapePrivate::KoShapePrivate(const KoShapePrivate &rhs, KoShape *q) : q_ptr(q), size(rhs.size), shapeId(rhs.shapeId), name(rhs.name), localMatrix(rhs.localMatrix), connectors(rhs.connectors), parent(0), // to be initialized later shapeManagers(), // to be initialized later toolDelegates(), // FIXME: how to initialize them? userData(rhs.userData ? rhs.userData->clone() : 0), stroke(rhs.stroke), fill(rhs.fill), inheritBackground(rhs.inheritBackground), inheritStroke(rhs.inheritStroke), dependees(), // FIXME: how to initialize them? shadow(0), // WARNING: not implemented in Krita border(0), // WARNING: not implemented in Krita clipPath(rhs.clipPath ? rhs.clipPath->clone() : 0), clipMask(rhs.clipMask ? rhs.clipMask->clone() : 0), additionalAttributes(rhs.additionalAttributes), additionalStyleAttributes(rhs.additionalStyleAttributes), filterEffectStack(0), // WARNING: not implemented in Krita transparency(rhs.transparency), hyperLink(rhs.hyperLink), zIndex(rhs.zIndex), runThrough(rhs.runThrough), visible(rhs.visible), printable(rhs.visible), geometryProtected(rhs.geometryProtected), keepAspect(rhs.keepAspect), selectable(rhs.selectable), detectCollision(rhs.detectCollision), protectContent(rhs.protectContent), textRunAroundSide(rhs.textRunAroundSide), textRunAroundDistanceLeft(rhs.textRunAroundDistanceLeft), textRunAroundDistanceTop(rhs.textRunAroundDistanceTop), textRunAroundDistanceRight(rhs.textRunAroundDistanceRight), textRunAroundDistanceBottom(rhs.textRunAroundDistanceBottom), textRunAroundThreshold(rhs.textRunAroundThreshold), textRunAroundContour(rhs.textRunAroundContour) { } KoShapePrivate::~KoShapePrivate() { Q_Q(KoShape); /** * The shape must have already been detached from all the parents and * shape managers. Otherwise we migh accidentally request some RTTI * information, which is not available anymore (we are in d-tor). * * TL;DR: fix the code that caused this destruction without unparenting * instead of trying to remove these assert! */ KIS_SAFE_ASSERT_RECOVER (!parent) { parent->removeShape(q); } KIS_SAFE_ASSERT_RECOVER (shapeManagers.isEmpty()) { Q_FOREACH (KoShapeManager *manager, shapeManagers) { manager->shapeInterface()->notifyShapeDestructed(q); } shapeManagers.clear(); } if (shadow && !shadow->deref()) delete shadow; if (filterEffectStack && !filterEffectStack->deref()) delete filterEffectStack; } void KoShapePrivate::shapeChanged(KoShape::ChangeType type) { Q_Q(KoShape); if (parent) parent->model()->childChanged(q, type); q->shapeChanged(type); Q_FOREACH (KoShape * shape, dependees) { shape->shapeChanged(type, q); } Q_FOREACH (KoShape::ShapeChangeListener *listener, listeners) { listener->notifyShapeChangedImpl(type, q); } } void KoShapePrivate::addShapeManager(KoShapeManager *manager) { shapeManagers.insert(manager); } void KoShapePrivate::removeShapeManager(KoShapeManager *manager) { shapeManagers.remove(manager); } void KoShapePrivate::convertFromShapeCoordinates(KoConnectionPoint &point, const QSizeF &shapeSize) const { switch(point.alignment) { case KoConnectionPoint::AlignNone: point.position = KoFlake::toRelative(point.position, shapeSize); point.position.rx() = qBound(0.0, point.position.x(), 1.0); point.position.ry() = qBound(0.0, point.position.y(), 1.0); break; case KoConnectionPoint::AlignRight: point.position.rx() -= shapeSize.width(); break; case KoConnectionPoint::AlignLeft: point.position.ry() = 0.5*shapeSize.height(); break; case KoConnectionPoint::AlignBottom: point.position.ry() -= shapeSize.height(); break; case KoConnectionPoint::AlignTop: point.position.rx() = 0.5*shapeSize.width(); break; case KoConnectionPoint::AlignTopLeft: // nothing to do here break; case KoConnectionPoint::AlignTopRight: point.position.rx() -= shapeSize.width(); break; case KoConnectionPoint::AlignBottomLeft: point.position.ry() -= shapeSize.height(); break; case KoConnectionPoint::AlignBottomRight: point.position.rx() -= shapeSize.width(); point.position.ry() -= shapeSize.height(); break; case KoConnectionPoint::AlignCenter: point.position.rx() -= 0.5 * shapeSize.width(); point.position.ry() -= 0.5 * shapeSize.height(); break; } } void KoShapePrivate::convertToShapeCoordinates(KoConnectionPoint &point, const QSizeF &shapeSize) const { switch(point.alignment) { case KoConnectionPoint::AlignNone: point.position = KoFlake::toAbsolute(point.position, shapeSize); break; case KoConnectionPoint::AlignRight: point.position.rx() += shapeSize.width(); break; case KoConnectionPoint::AlignLeft: point.position.ry() = 0.5*shapeSize.height(); break; case KoConnectionPoint::AlignBottom: point.position.ry() += shapeSize.height(); break; case KoConnectionPoint::AlignTop: point.position.rx() = 0.5*shapeSize.width(); break; case KoConnectionPoint::AlignTopLeft: // nothing to do here break; case KoConnectionPoint::AlignTopRight: point.position.rx() += shapeSize.width(); break; case KoConnectionPoint::AlignBottomLeft: point.position.ry() += shapeSize.height(); break; case KoConnectionPoint::AlignBottomRight: point.position.rx() += shapeSize.width(); point.position.ry() += shapeSize.height(); break; case KoConnectionPoint::AlignCenter: point.position.rx() += 0.5 * shapeSize.width(); point.position.ry() += 0.5 * shapeSize.height(); break; } } // static QString KoShapePrivate::getStyleProperty(const char *property, KoShapeLoadingContext &context) { KoStyleStack &styleStack = context.odfLoadingContext().styleStack(); QString value; if (styleStack.hasProperty(KoXmlNS::draw, property)) { value = styleStack.property(KoXmlNS::draw, property); } return value; } // ======== KoShape const qint16 KoShape::maxZIndex = std::numeric_limits::max(); const qint16 KoShape::minZIndex = std::numeric_limits::min(); KoShape::KoShape() : d_ptr(new KoShapePrivate(this)) { notifyChanged(); } KoShape::KoShape(KoShapePrivate *dd) : d_ptr(dd) { } KoShape::~KoShape() { Q_D(KoShape); d->shapeChanged(Deleted); d->listeners.clear(); delete d_ptr; } KoShape *KoShape::cloneShape() const { KIS_SAFE_ASSERT_RECOVER_NOOP(0 && "not implemented!"); qWarning() << shapeId() << "cannot be cloned"; return 0; } void KoShape::paintStroke(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintcontext) { Q_UNUSED(paintcontext); if (stroke()) { stroke()->paint(this, painter, converter); } } void KoShape::scale(qreal sx, qreal sy) { Q_D(KoShape); QPointF pos = position(); QTransform scaleMatrix; scaleMatrix.translate(pos.x(), pos.y()); scaleMatrix.scale(sx, sy); scaleMatrix.translate(-pos.x(), -pos.y()); d->localMatrix = d->localMatrix * scaleMatrix; notifyChanged(); d->shapeChanged(ScaleChanged); } void KoShape::rotate(qreal angle) { Q_D(KoShape); QPointF center = d->localMatrix.map(QPointF(0.5 * size().width(), 0.5 * size().height())); QTransform rotateMatrix; rotateMatrix.translate(center.x(), center.y()); rotateMatrix.rotate(angle); rotateMatrix.translate(-center.x(), -center.y()); d->localMatrix = d->localMatrix * rotateMatrix; notifyChanged(); d->shapeChanged(RotationChanged); } void KoShape::shear(qreal sx, qreal sy) { Q_D(KoShape); QPointF pos = position(); QTransform shearMatrix; shearMatrix.translate(pos.x(), pos.y()); shearMatrix.shear(sx, sy); shearMatrix.translate(-pos.x(), -pos.y()); d->localMatrix = d->localMatrix * shearMatrix; notifyChanged(); d->shapeChanged(ShearChanged); } void KoShape::setSize(const QSizeF &newSize) { Q_D(KoShape); QSizeF oldSize(size()); // always set size, as d->size and size() may vary d->size = newSize; if (oldSize == newSize) return; notifyChanged(); d->shapeChanged(SizeChanged); } void KoShape::setPosition(const QPointF &newPosition) { Q_D(KoShape); QPointF currentPos = position(); if (newPosition == currentPos) return; QTransform translateMatrix; translateMatrix.translate(newPosition.x() - currentPos.x(), newPosition.y() - currentPos.y()); d->localMatrix = d->localMatrix * translateMatrix; notifyChanged(); d->shapeChanged(PositionChanged); } bool KoShape::hitTest(const QPointF &position) const { Q_D(const KoShape); if (d->parent && d->parent->isClipped(this) && !d->parent->hitTest(position)) return false; QPointF point = absoluteTransformation(0).inverted().map(position); QRectF bb = outlineRect(); if (d->stroke) { KoInsets insets; d->stroke->strokeInsets(this, insets); bb.adjust(-insets.left, -insets.top, insets.right, insets.bottom); } if (bb.contains(point)) return true; // if there is no shadow we can as well just leave if (! d->shadow) return false; // the shadow has an offset to the shape, so we simply // check if the position minus the shadow offset hits the shape point = absoluteTransformation(0).inverted().map(position - d->shadow->offset()); return bb.contains(point); } QRectF KoShape::boundingRect() const { Q_D(const KoShape); QTransform transform = absoluteTransformation(0); QRectF bb = outlineRect(); if (d->stroke) { KoInsets insets; d->stroke->strokeInsets(this, insets); bb.adjust(-insets.left, -insets.top, insets.right, insets.bottom); } bb = transform.mapRect(bb); if (d->shadow) { KoInsets insets; d->shadow->insets(insets); bb.adjust(-insets.left, -insets.top, insets.right, insets.bottom); } if (d->filterEffectStack) { QRectF clipRect = d->filterEffectStack->clipRectForBoundingRect(outlineRect()); bb |= transform.mapRect(clipRect); } return bb; } QRectF KoShape::boundingRect(const QList &shapes) { QRectF boundingRect; Q_FOREACH (KoShape *shape, shapes) { boundingRect |= shape->boundingRect(); } return boundingRect; } QRectF KoShape::absoluteOutlineRect(KoViewConverter *converter) const { return absoluteTransformation(converter).map(outline()).boundingRect(); } QRectF KoShape::absoluteOutlineRect(const QList &shapes, KoViewConverter *converter) { QRectF absoluteOutlineRect; Q_FOREACH (KoShape *shape, shapes) { absoluteOutlineRect |= shape->absoluteOutlineRect(converter); } return absoluteOutlineRect; } QTransform KoShape::absoluteTransformation(const KoViewConverter *converter) const { Q_D(const KoShape); QTransform matrix; // apply parents matrix to inherit any transformations done there. KoShapeContainer * container = d->parent; if (container) { if (container->inheritsTransform(this)) { // We do need to pass the converter here, otherwise the parent's // translation is not inherited. matrix = container->absoluteTransformation(converter); } else { QSizeF containerSize = container->size(); QPointF containerPos = container->absolutePosition() - QPointF(0.5 * containerSize.width(), 0.5 * containerSize.height()); if (converter) containerPos = converter->documentToView(containerPos); matrix.translate(containerPos.x(), containerPos.y()); } } if (converter) { QPointF pos = d->localMatrix.map(QPointF()); QPointF trans = converter->documentToView(pos) - pos; matrix.translate(trans.x(), trans.y()); } return d->localMatrix * matrix; } void KoShape::applyAbsoluteTransformation(const QTransform &matrix) { QTransform globalMatrix = absoluteTransformation(0); // the transformation is relative to the global coordinate system // but we want to change the local matrix, so convert the matrix // to be relative to the local coordinate system QTransform transformMatrix = globalMatrix * matrix * globalMatrix.inverted(); applyTransformation(transformMatrix); } void KoShape::applyTransformation(const QTransform &matrix) { Q_D(KoShape); d->localMatrix = matrix * d->localMatrix; notifyChanged(); d->shapeChanged(GenericMatrixChange); } void KoShape::setTransformation(const QTransform &matrix) { Q_D(KoShape); d->localMatrix = matrix; notifyChanged(); d->shapeChanged(GenericMatrixChange); } QTransform KoShape::transformation() const { Q_D(const KoShape); return d->localMatrix; } KoShape::ChildZOrderPolicy KoShape::childZOrderPolicy() { return ChildZDefault; } bool KoShape::compareShapeZIndex(KoShape *s1, KoShape *s2) { /** * WARNING: Our definition of zIndex is not yet compatible with SVG2's * definition. In SVG stacking context of groups with the same * zIndex are **merged**, while in Krita the contents of groups * is never merged. One group will always below than the other. * Therefore, when zIndex of two groups inside the same parent - * coinside, the resulting painting order in Krita is + * coincide, the resulting painting order in Krita is * **UNDEFINED**. * * To avoid this trouble we use KoShapeReorderCommand::mergeInShape() * inside KoShapeCreateCommand. */ /** * The algorithm below doesn't correctly handle the case when the two pointers actually * point to the same shape. So just check it in advance to guarantee strict weak ordering * relation requirement */ if (s1 == s2) return false; // First sort according to runThrough which is sort of a master level KoShape *parentShapeS1 = s1->parent(); KoShape *parentShapeS2 = s2->parent(); int runThrough1 = s1->runThrough(); int runThrough2 = s2->runThrough(); while (parentShapeS1) { if (parentShapeS1->childZOrderPolicy() == KoShape::ChildZParentChild) { runThrough1 = parentShapeS1->runThrough(); } else { runThrough1 = runThrough1 + parentShapeS1->runThrough(); } parentShapeS1 = parentShapeS1->parent(); } while (parentShapeS2) { if (parentShapeS2->childZOrderPolicy() == KoShape::ChildZParentChild) { runThrough2 = parentShapeS2->runThrough(); } else { runThrough2 = runThrough2 + parentShapeS2->runThrough(); } parentShapeS2 = parentShapeS2->parent(); } if (runThrough1 > runThrough2) { return false; } if (runThrough1 < runThrough2) { return true; } // If on the same runThrough level then the zIndex is all that matters. // // We basically walk up through the parents until we find a common base parent // To do that we need two loops where the inner loop walks up through the parents // of s2 every time we step up one parent level on s1 // // We don't update the index value until after we have seen that it's not a common base // That way we ensure that two children of a common base are sorted according to their respective // z value bool foundCommonParent = false; int index1 = s1->zIndex(); int index2 = s2->zIndex(); parentShapeS1 = s1; parentShapeS2 = s2; while (parentShapeS1 && !foundCommonParent) { parentShapeS2 = s2; index2 = parentShapeS2->zIndex(); while (parentShapeS2) { if (parentShapeS2 == parentShapeS1) { foundCommonParent = true; break; } if (parentShapeS2->childZOrderPolicy() == KoShape::ChildZParentChild) { index2 = parentShapeS2->zIndex(); } parentShapeS2 = parentShapeS2->parent(); } if (!foundCommonParent) { if (parentShapeS1->childZOrderPolicy() == KoShape::ChildZParentChild) { index1 = parentShapeS1->zIndex(); } parentShapeS1 = parentShapeS1->parent(); } } // If the one shape is a parent/child of the other then sort so. if (s1 == parentShapeS2) { return true; } if (s2 == parentShapeS1) { return false; } // If we went that far then the z-Index is used for sorting. return index1 < index2; } void KoShape::setParent(KoShapeContainer *parent) { Q_D(KoShape); if (d->parent == parent) { return; } KoShapeContainer *oldParent = d->parent; d->parent = 0; // avoids recursive removing if (oldParent) { oldParent->shapeInterface()->removeShape(this); } KIS_SAFE_ASSERT_RECOVER_NOOP(parent != this); if (parent && parent != this) { d->parent = parent; parent->shapeInterface()->addShape(this); } notifyChanged(); d->shapeChanged(ParentChanged); } bool KoShape::inheritsTransformFromAny(const QList ancestorsInQuestion) const { bool result = false; KoShape *shape = const_cast(this); while (shape) { KoShapeContainer *parent = shape->parent(); if (parent && !parent->inheritsTransform(shape)) { break; } if (ancestorsInQuestion.contains(shape)) { result = true; break; } shape = parent; } return result; } bool KoShape::hasCommonParent(const KoShape *shape) const { const KoShape *thisShape = this; while (thisShape) { const KoShape *otherShape = shape; while (otherShape) { if (thisShape == otherShape) { return true; } otherShape = otherShape->parent(); } thisShape = thisShape->parent(); } return false; } qint16 KoShape::zIndex() const { Q_D(const KoShape); return d->zIndex; } void KoShape::update() const { Q_D(const KoShape); if (!d->shapeManagers.empty()) { QRectF rect(boundingRect()); Q_FOREACH (KoShapeManager * manager, d->shapeManagers) { manager->update(rect, this, true); } } } void KoShape::updateAbsolute(const QRectF &rect) const { if (rect.isEmpty() && !rect.isNull()) { return; } Q_D(const KoShape); if (!d->shapeManagers.empty() && isVisible()) { Q_FOREACH (KoShapeManager * manager, d->shapeManagers) { manager->update(rect); } } } QPainterPath KoShape::outline() const { QPainterPath path; path.addRect(outlineRect()); return path; } QRectF KoShape::outlineRect() const { const QSizeF s = size(); return QRectF(QPointF(0, 0), QSizeF(qMax(s.width(), qreal(0.0001)), qMax(s.height(), qreal(0.0001)))); } QPainterPath KoShape::shadowOutline() const { Q_D(const KoShape); if (background()) { return outline(); } return QPainterPath(); } QPointF KoShape::absolutePosition(KoFlake::AnchorPosition anchor) const { const QRectF rc = outlineRect(); QPointF point = rc.topLeft(); bool valid = false; QPointF anchoredPoint = KoFlake::anchorToPoint(anchor, rc, &valid); if (valid) { point = anchoredPoint; } return absoluteTransformation(0).map(point); } void KoShape::setAbsolutePosition(const QPointF &newPosition, KoFlake::AnchorPosition anchor) { Q_D(KoShape); QPointF currentAbsPosition = absolutePosition(anchor); QPointF translate = newPosition - currentAbsPosition; QTransform translateMatrix; translateMatrix.translate(translate.x(), translate.y()); applyAbsoluteTransformation(translateMatrix); notifyChanged(); d->shapeChanged(PositionChanged); } void KoShape::copySettings(const KoShape *shape) { Q_D(KoShape); d->size = shape->size(); d->connectors.clear(); Q_FOREACH (const KoConnectionPoint &point, shape->connectionPoints()) addConnectionPoint(point); d->zIndex = shape->zIndex(); d->visible = shape->isVisible(false); // Ensure printable is true by default if (!d->visible) d->printable = true; else d->printable = shape->isPrintable(); d->geometryProtected = shape->isGeometryProtected(); d->protectContent = shape->isContentProtected(); d->selectable = shape->isSelectable(); d->keepAspect = shape->keepAspectRatio(); d->localMatrix = shape->d_ptr->localMatrix; } void KoShape::notifyChanged() { Q_D(KoShape); Q_FOREACH (KoShapeManager * manager, d->shapeManagers) { manager->notifyShapeChanged(this); } } void KoShape::setUserData(KoShapeUserData *userData) { Q_D(KoShape); d->userData.reset(userData); } KoShapeUserData *KoShape::userData() const { Q_D(const KoShape); return d->userData.data(); } bool KoShape::hasTransparency() const { Q_D(const KoShape); QSharedPointer bg = background(); return !bg || bg->hasTransparency() || d->transparency > 0.0; } void KoShape::setTransparency(qreal transparency) { Q_D(KoShape); d->transparency = qBound(0.0, transparency, 1.0); d->shapeChanged(TransparencyChanged); notifyChanged(); } qreal KoShape::transparency(bool recursive) const { Q_D(const KoShape); if (!recursive || !parent()) { return d->transparency; } else { const qreal parentOpacity = 1.0-parent()->transparency(recursive); const qreal childOpacity = 1.0-d->transparency; return 1.0-(parentOpacity*childOpacity); } } KoInsets KoShape::strokeInsets() const { Q_D(const KoShape); KoInsets answer; if (d->stroke) d->stroke->strokeInsets(this, answer); return answer; } qreal KoShape::rotation() const { Q_D(const KoShape); // try to extract the rotation angle out of the local matrix // if it is a pure rotation matrix // check if the matrix has shearing mixed in if (fabs(fabs(d->localMatrix.m12()) - fabs(d->localMatrix.m21())) > 1e-10) return std::numeric_limits::quiet_NaN(); // check if the matrix has scaling mixed in if (fabs(d->localMatrix.m11() - d->localMatrix.m22()) > 1e-10) return std::numeric_limits::quiet_NaN(); // calculate the angle from the matrix elements qreal angle = atan2(-d->localMatrix.m21(), d->localMatrix.m11()) * 180.0 / M_PI; if (angle < 0.0) angle += 360.0; return angle; } QSizeF KoShape::size() const { Q_D(const KoShape); return d->size; } QPointF KoShape::position() const { Q_D(const KoShape); QPointF center = outlineRect().center(); return d->localMatrix.map(center) - center; } int KoShape::addConnectionPoint(const KoConnectionPoint &point) { Q_D(KoShape); // get next glue point id int nextConnectionPointId = KoConnectionPoint::FirstCustomConnectionPoint; if (d->connectors.size()) nextConnectionPointId = qMax(nextConnectionPointId, (--d->connectors.end()).key()+1); KoConnectionPoint p = point; d->convertFromShapeCoordinates(p, size()); d->connectors[nextConnectionPointId] = p; return nextConnectionPointId; } bool KoShape::setConnectionPoint(int connectionPointId, const KoConnectionPoint &point) { Q_D(KoShape); if (connectionPointId < 0) return false; const bool insertPoint = !hasConnectionPoint(connectionPointId); switch(connectionPointId) { case KoConnectionPoint::TopConnectionPoint: case KoConnectionPoint::RightConnectionPoint: case KoConnectionPoint::BottomConnectionPoint: case KoConnectionPoint::LeftConnectionPoint: { KoConnectionPoint::PointId id = static_cast(connectionPointId); d->connectors[id] = KoConnectionPoint::defaultConnectionPoint(id); break; } default: { KoConnectionPoint p = point; d->convertFromShapeCoordinates(p, size()); d->connectors[connectionPointId] = p; break; } } if(!insertPoint) d->shapeChanged(ConnectionPointChanged); return true; } bool KoShape::hasConnectionPoint(int connectionPointId) const { Q_D(const KoShape); return d->connectors.contains(connectionPointId); } KoConnectionPoint KoShape::connectionPoint(int connectionPointId) const { Q_D(const KoShape); KoConnectionPoint p = d->connectors.value(connectionPointId, KoConnectionPoint()); // convert glue point to shape coordinates d->convertToShapeCoordinates(p, size()); return p; } KoConnectionPoints KoShape::connectionPoints() const { Q_D(const KoShape); QSizeF s = size(); KoConnectionPoints points = d->connectors; KoConnectionPoints::iterator point = points.begin(); KoConnectionPoints::iterator lastPoint = points.end(); // convert glue points to shape coordinates for(; point != lastPoint; ++point) { d->convertToShapeCoordinates(point.value(), s); } return points; } void KoShape::removeConnectionPoint(int connectionPointId) { Q_D(KoShape); d->connectors.remove(connectionPointId); d->shapeChanged(ConnectionPointChanged); } void KoShape::clearConnectionPoints() { Q_D(KoShape); d->connectors.clear(); } KoShape::TextRunAroundSide KoShape::textRunAroundSide() const { Q_D(const KoShape); return d->textRunAroundSide; } void KoShape::setTextRunAroundSide(TextRunAroundSide side, RunThroughLevel runThrought) { Q_D(KoShape); if (side == RunThrough) { if (runThrought == Background) { setRunThrough(-1); } else { setRunThrough(1); } } else { setRunThrough(0); } if ( d->textRunAroundSide == side) { return; } d->textRunAroundSide = side; notifyChanged(); d->shapeChanged(TextRunAroundChanged); } qreal KoShape::textRunAroundDistanceTop() const { Q_D(const KoShape); return d->textRunAroundDistanceTop; } void KoShape::setTextRunAroundDistanceTop(qreal distance) { Q_D(KoShape); d->textRunAroundDistanceTop = distance; } qreal KoShape::textRunAroundDistanceLeft() const { Q_D(const KoShape); return d->textRunAroundDistanceLeft; } void KoShape::setTextRunAroundDistanceLeft(qreal distance) { Q_D(KoShape); d->textRunAroundDistanceLeft = distance; } qreal KoShape::textRunAroundDistanceRight() const { Q_D(const KoShape); return d->textRunAroundDistanceRight; } void KoShape::setTextRunAroundDistanceRight(qreal distance) { Q_D(KoShape); d->textRunAroundDistanceRight = distance; } qreal KoShape::textRunAroundDistanceBottom() const { Q_D(const KoShape); return d->textRunAroundDistanceBottom; } void KoShape::setTextRunAroundDistanceBottom(qreal distance) { Q_D(KoShape); d->textRunAroundDistanceBottom = distance; } qreal KoShape::textRunAroundThreshold() const { Q_D(const KoShape); return d->textRunAroundThreshold; } void KoShape::setTextRunAroundThreshold(qreal threshold) { Q_D(KoShape); d->textRunAroundThreshold = threshold; } KoShape::TextRunAroundContour KoShape::textRunAroundContour() const { Q_D(const KoShape); return d->textRunAroundContour; } void KoShape::setTextRunAroundContour(KoShape::TextRunAroundContour contour) { Q_D(KoShape); d->textRunAroundContour = contour; } void KoShape::setBackground(QSharedPointer fill) { Q_D(KoShape); d->inheritBackground = false; d->fill = fill; d->shapeChanged(BackgroundChanged); notifyChanged(); } QSharedPointer KoShape::background() const { Q_D(const KoShape); QSharedPointer bg; if (!d->inheritBackground) { bg = d->fill; } else if (parent()) { bg = parent()->background(); } return bg; } void KoShape::setInheritBackground(bool value) { Q_D(KoShape); d->inheritBackground = value; if (d->inheritBackground) { d->fill.clear(); } } bool KoShape::inheritBackground() const { Q_D(const KoShape); return d->inheritBackground; } void KoShape::setZIndex(qint16 zIndex) { Q_D(KoShape); if (d->zIndex == zIndex) return; d->zIndex = zIndex; notifyChanged(); } int KoShape::runThrough() { Q_D(const KoShape); return d->runThrough; } void KoShape::setRunThrough(short int runThrough) { Q_D(KoShape); d->runThrough = runThrough; } void KoShape::setVisible(bool on) { Q_D(KoShape); int _on = (on ? 1 : 0); if (d->visible == _on) return; d->visible = _on; } bool KoShape::isVisible(bool recursive) const { Q_D(const KoShape); if (!recursive) return d->visible; if (!d->visible) return false; KoShapeContainer * parentShape = parent(); if (parentShape) { return parentShape->isVisible(true); } return true; } void KoShape::setPrintable(bool on) { Q_D(KoShape); d->printable = on; } bool KoShape::isPrintable() const { Q_D(const KoShape); if (d->visible) return d->printable; else return false; } void KoShape::setSelectable(bool selectable) { Q_D(KoShape); d->selectable = selectable; } bool KoShape::isSelectable() const { Q_D(const KoShape); return d->selectable; } void KoShape::setGeometryProtected(bool on) { Q_D(KoShape); d->geometryProtected = on; } bool KoShape::isGeometryProtected() const { Q_D(const KoShape); return d->geometryProtected; } void KoShape::setContentProtected(bool protect) { Q_D(KoShape); d->protectContent = protect; } bool KoShape::isContentProtected() const { Q_D(const KoShape); return d->protectContent; } KoShapeContainer *KoShape::parent() const { Q_D(const KoShape); return d->parent; } void KoShape::setKeepAspectRatio(bool keepAspect) { Q_D(KoShape); d->keepAspect = keepAspect; d->shapeChanged(KeepAspectRatioChange); notifyChanged(); } bool KoShape::keepAspectRatio() const { Q_D(const KoShape); return d->keepAspect; } QString KoShape::shapeId() const { Q_D(const KoShape); return d->shapeId; } void KoShape::setShapeId(const QString &id) { Q_D(KoShape); d->shapeId = id; } void KoShape::setCollisionDetection(bool detect) { Q_D(KoShape); d->detectCollision = detect; } bool KoShape::collisionDetection() { Q_D(KoShape); return d->detectCollision; } KoShapeStrokeModelSP KoShape::stroke() const { Q_D(const KoShape); KoShapeStrokeModelSP stroke; if (!d->inheritStroke) { stroke = d->stroke; } else if (parent()) { stroke = parent()->stroke(); } return stroke; } void KoShape::setStroke(KoShapeStrokeModelSP stroke) { Q_D(KoShape); d->inheritStroke = false; d->stroke = stroke; d->shapeChanged(StrokeChanged); notifyChanged(); } void KoShape::setInheritStroke(bool value) { Q_D(KoShape); d->inheritStroke = value; if (d->inheritStroke) { d->stroke.clear(); } } bool KoShape::inheritStroke() const { Q_D(const KoShape); return d->inheritStroke; } void KoShape::setShadow(KoShapeShadow *shadow) { Q_D(KoShape); if (d->shadow) d->shadow->deref(); d->shadow = shadow; if (d->shadow) { d->shadow->ref(); // TODO update changed area } d->shapeChanged(ShadowChanged); notifyChanged(); } KoShapeShadow *KoShape::shadow() const { Q_D(const KoShape); return d->shadow; } void KoShape::setBorder(KoBorder *border) { Q_D(KoShape); if (d->border) { // The shape owns the border. delete d->border; } d->border = border; d->shapeChanged(BorderChanged); notifyChanged(); } KoBorder *KoShape::border() const { Q_D(const KoShape); return d->border; } void KoShape::setClipPath(KoClipPath *clipPath) { Q_D(KoShape); d->clipPath.reset(clipPath); d->shapeChanged(ClipPathChanged); notifyChanged(); } KoClipPath * KoShape::clipPath() const { Q_D(const KoShape); return d->clipPath.data(); } void KoShape::setClipMask(KoClipMask *clipMask) { Q_D(KoShape); d->clipMask.reset(clipMask); } KoClipMask* KoShape::clipMask() const { Q_D(const KoShape); return d->clipMask.data(); } QTransform KoShape::transform() const { Q_D(const KoShape); return d->localMatrix; } QString KoShape::name() const { Q_D(const KoShape); return d->name; } void KoShape::setName(const QString &name) { Q_D(KoShape); d->name = name; } void KoShape::waitUntilReady(const KoViewConverter &converter, bool asynchronous) const { Q_UNUSED(converter); Q_UNUSED(asynchronous); } bool KoShape::isShapeEditable(bool recursive) const { Q_D(const KoShape); if (!d->visible || d->geometryProtected) return false; if (recursive && d->parent) { return d->parent->isShapeEditable(true); } return true; } // painting void KoShape::paintBorder(QPainter &painter, const KoViewConverter &converter) { Q_UNUSED(converter); KoBorder *bd = border(); if (!bd) { return; } QRectF borderRect = QRectF(QPointF(0, 0), size()); // Paint the border. bd->paint(painter, borderRect, KoBorder::PaintInsideLine); } // loading & saving methods QString KoShape::saveStyle(KoGenStyle &style, KoShapeSavingContext &context) const { Q_D(const KoShape); // and fill the style KoShapeStrokeModelSP sm = stroke(); if (sm) { sm->fillStyle(style, context); } else { style.addProperty("draw:stroke", "none", KoGenStyle::GraphicType); } KoShapeShadow *s = shadow(); if (s) s->fillStyle(style, context); QSharedPointer bg = background(); if (bg) { bg->fillStyle(style, context); } else { style.addProperty("draw:fill", "none", KoGenStyle::GraphicType); } KoBorder *b = border(); if (b) { b->saveOdf(style); } if (context.isSet(KoShapeSavingContext::AutoStyleInStyleXml)) { style.setAutoStyleInStylesDotXml(true); } QString value; if (isGeometryProtected()) { value = "position size"; } if (isContentProtected()) { if (! value.isEmpty()) value += ' '; value += "content"; } if (!value.isEmpty()) { style.addProperty("style:protect", value, KoGenStyle::GraphicType); } QMap::const_iterator it(d->additionalStyleAttributes.constBegin()); for (; it != d->additionalStyleAttributes.constEnd(); ++it) { style.addProperty(it.key(), it.value()); } if (parent() && parent()->isClipped(this)) { /* * In Calligra clipping is done using a parent shape which can be rotated, sheared etc * and even non-square. So the ODF interoperability version we write here is really * just a very simple version of that... */ qreal top = -position().y(); qreal left = -position().x(); qreal right = parent()->size().width() - size().width() - left; qreal bottom = parent()->size().height() - size().height() - top; style.addProperty("fo:clip", QString("rect(%1pt, %2pt, %3pt, %4pt)") .arg(top, 10, 'f').arg(right, 10, 'f') .arg(bottom, 10, 'f').arg(left, 10, 'f'), KoGenStyle::GraphicType); } QString wrap; switch (textRunAroundSide()) { case BiggestRunAroundSide: wrap = "biggest"; break; case LeftRunAroundSide: wrap = "left"; break; case RightRunAroundSide: wrap = "right"; break; case EnoughRunAroundSide: wrap = "dynamic"; break; case BothRunAroundSide: wrap = "parallel"; break; case NoRunAround: wrap = "none"; break; case RunThrough: wrap = "run-through"; break; } style.addProperty("style:wrap", wrap, KoGenStyle::GraphicType); switch (textRunAroundContour()) { case ContourBox: style.addProperty("style:wrap-contour", "false", KoGenStyle::GraphicType); break; case ContourFull: style.addProperty("style:wrap-contour", "true", KoGenStyle::GraphicType); style.addProperty("style:wrap-contour-mode", "full", KoGenStyle::GraphicType); break; case ContourOutside: style.addProperty("style:wrap-contour", "true", KoGenStyle::GraphicType); style.addProperty("style:wrap-contour-mode", "outside", KoGenStyle::GraphicType); break; } style.addPropertyPt("style:wrap-dynamic-threshold", textRunAroundThreshold(), KoGenStyle::GraphicType); if ((textRunAroundDistanceLeft() == textRunAroundDistanceRight()) && (textRunAroundDistanceTop() == textRunAroundDistanceBottom()) && (textRunAroundDistanceLeft() == textRunAroundDistanceTop())) { style.addPropertyPt("fo:margin", textRunAroundDistanceLeft(), KoGenStyle::GraphicType); } else { style.addPropertyPt("fo:margin-left", textRunAroundDistanceLeft(), KoGenStyle::GraphicType); style.addPropertyPt("fo:margin-top", textRunAroundDistanceTop(), KoGenStyle::GraphicType); style.addPropertyPt("fo:margin-right", textRunAroundDistanceRight(), KoGenStyle::GraphicType); style.addPropertyPt("fo:margin-bottom", textRunAroundDistanceBottom(), KoGenStyle::GraphicType); } return context.mainStyles().insert(style, context.isSet(KoShapeSavingContext::PresentationShape) ? "pr" : "gr"); } void KoShape::loadStyle(const KoXmlElement &element, KoShapeLoadingContext &context) { Q_D(KoShape); KoStyleStack &styleStack = context.odfLoadingContext().styleStack(); styleStack.setTypeProperties("graphic"); d->fill.clear(); d->stroke.clear(); if (d->shadow && !d->shadow->deref()) { delete d->shadow; d->shadow = 0; } setBackground(loadOdfFill(context)); setStroke(loadOdfStroke(element, context)); setShadow(d->loadOdfShadow(context)); setBorder(d->loadOdfBorder(context)); QString protect(styleStack.property(KoXmlNS::style, "protect")); setGeometryProtected(protect.contains("position") || protect.contains("size")); setContentProtected(protect.contains("content")); QString margin = styleStack.property(KoXmlNS::fo, "margin"); if (!margin.isEmpty()) { setTextRunAroundDistanceLeft(KoUnit::parseValue(margin)); setTextRunAroundDistanceTop(KoUnit::parseValue(margin)); setTextRunAroundDistanceRight(KoUnit::parseValue(margin)); setTextRunAroundDistanceBottom(KoUnit::parseValue(margin)); } margin = styleStack.property(KoXmlNS::fo, "margin-left"); if (!margin.isEmpty()) { setTextRunAroundDistanceLeft(KoUnit::parseValue(margin)); } margin = styleStack.property(KoXmlNS::fo, "margin-top"); if (!margin.isEmpty()) { setTextRunAroundDistanceTop(KoUnit::parseValue(margin)); } margin = styleStack.property(KoXmlNS::fo, "margin-right"); if (!margin.isEmpty()) { setTextRunAroundDistanceRight(KoUnit::parseValue(margin)); } margin = styleStack.property(KoXmlNS::fo, "margin-bottom"); if (!margin.isEmpty()) { setTextRunAroundDistanceBottom(KoUnit::parseValue(margin)); } QString wrap; if (styleStack.hasProperty(KoXmlNS::style, "wrap")) { wrap = styleStack.property(KoXmlNS::style, "wrap"); } else { // no value given in the file, but guess biggest wrap = "biggest"; } if (wrap == "none") { setTextRunAroundSide(KoShape::NoRunAround); } else if (wrap == "run-through") { QString runTrought = styleStack.property(KoXmlNS::style, "run-through", "background"); if (runTrought == "background") { setTextRunAroundSide(KoShape::RunThrough, KoShape::Background); } else { setTextRunAroundSide(KoShape::RunThrough, KoShape::Foreground); } } else { if (wrap == "biggest") setTextRunAroundSide(KoShape::BiggestRunAroundSide); else if (wrap == "left") setTextRunAroundSide(KoShape::LeftRunAroundSide); else if (wrap == "right") setTextRunAroundSide(KoShape::RightRunAroundSide); else if (wrap == "dynamic") setTextRunAroundSide(KoShape::EnoughRunAroundSide); else if (wrap == "parallel") setTextRunAroundSide(KoShape::BothRunAroundSide); } if (styleStack.hasProperty(KoXmlNS::style, "wrap-dynamic-threshold")) { QString wrapThreshold = styleStack.property(KoXmlNS::style, "wrap-dynamic-threshold"); if (!wrapThreshold.isEmpty()) { setTextRunAroundThreshold(KoUnit::parseValue(wrapThreshold)); } } if (styleStack.property(KoXmlNS::style, "wrap-contour", "false") == "true") { if (styleStack.property(KoXmlNS::style, "wrap-contour-mode", "full") == "full") { setTextRunAroundContour(KoShape::ContourFull); } else { setTextRunAroundContour(KoShape::ContourOutside); } } else { setTextRunAroundContour(KoShape::ContourBox); } } bool KoShape::loadOdfAttributes(const KoXmlElement &element, KoShapeLoadingContext &context, int attributes) { if (attributes & OdfPosition) { QPointF pos(position()); if (element.hasAttributeNS(KoXmlNS::svg, "x")) pos.setX(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "x", QString()))); if (element.hasAttributeNS(KoXmlNS::svg, "y")) pos.setY(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "y", QString()))); setPosition(pos); } if (attributes & OdfSize) { QSizeF s(size()); if (element.hasAttributeNS(KoXmlNS::svg, "width")) s.setWidth(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "width", QString()))); if (element.hasAttributeNS(KoXmlNS::svg, "height")) s.setHeight(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "height", QString()))); setSize(s); } if (attributes & OdfLayer) { if (element.hasAttributeNS(KoXmlNS::draw, "layer")) { KoShapeLayer *layer = context.layer(element.attributeNS(KoXmlNS::draw, "layer")); if (layer) { setParent(layer); } } } if (attributes & OdfId) { KoElementReference ref; ref.loadOdf(element); if (ref.isValid()) { context.addShapeId(this, ref.toString()); } } if (attributes & OdfZIndex) { if (element.hasAttributeNS(KoXmlNS::draw, "z-index")) { setZIndex(element.attributeNS(KoXmlNS::draw, "z-index").toInt()); } else { setZIndex(context.zIndex()); } } if (attributes & OdfName) { if (element.hasAttributeNS(KoXmlNS::draw, "name")) { setName(element.attributeNS(KoXmlNS::draw, "name")); } } if (attributes & OdfStyle) { KoStyleStack &styleStack = context.odfLoadingContext().styleStack(); styleStack.save(); if (element.hasAttributeNS(KoXmlNS::draw, "style-name")) { context.odfLoadingContext().fillStyleStack(element, KoXmlNS::draw, "style-name", "graphic"); } if (element.hasAttributeNS(KoXmlNS::presentation, "style-name")) { context.odfLoadingContext().fillStyleStack(element, KoXmlNS::presentation, "style-name", "presentation"); } loadStyle(element, context); styleStack.restore(); } if (attributes & OdfTransformation) { QString transform = element.attributeNS(KoXmlNS::draw, "transform", QString()); if (! transform.isEmpty()) applyAbsoluteTransformation(parseOdfTransform(transform)); } if (attributes & OdfAdditionalAttributes) { QSet additionalAttributeData = KoShapeLoadingContext::additionalAttributeData(); Q_FOREACH (const KoShapeLoadingContext::AdditionalAttributeData &attributeData, additionalAttributeData) { if (element.hasAttributeNS(attributeData.ns, attributeData.tag)) { QString value = element.attributeNS(attributeData.ns, attributeData.tag); //debugFlake << "load additional attribute" << attributeData.tag << value; setAdditionalAttribute(attributeData.name, value); } } } if (attributes & OdfCommonChildElements) { // load glue points (connection points) loadOdfGluePoints(element, context); } return true; } QSharedPointer KoShape::loadOdfFill(KoShapeLoadingContext &context) const { QString fill = KoShapePrivate::getStyleProperty("fill", context); QSharedPointer bg; if (fill == "solid") { bg = QSharedPointer(new KoColorBackground()); } else if (fill == "hatch") { bg = QSharedPointer(new KoHatchBackground()); } else if (fill == "gradient") { QString styleName = KoShapePrivate::getStyleProperty("fill-gradient-name", context); KoXmlElement *e = context.odfLoadingContext().stylesReader().drawStyles("gradient")[styleName]; QString style; if (e) { style = e->attributeNS(KoXmlNS::draw, "style", QString()); } if ((style == "rectangular") || (style == "square")) { bg = QSharedPointer(new KoOdfGradientBackground()); } else { QGradient *gradient = new QLinearGradient(); gradient->setCoordinateMode(QGradient::ObjectBoundingMode); bg = QSharedPointer(new KoGradientBackground(gradient)); } } else if (fill == "bitmap") { bg = QSharedPointer(new KoPatternBackground(context.imageCollection())); #ifndef NWORKAROUND_ODF_BUGS } else if (fill.isEmpty()) { bg = QSharedPointer(KoOdfWorkaround::fixBackgroundColor(this, context)); return bg; #endif } else { return QSharedPointer(0); } if (!bg->loadStyle(context.odfLoadingContext(), size())) { return QSharedPointer(0); } return bg; } KoShapeStrokeModelSP KoShape::loadOdfStroke(const KoXmlElement &element, KoShapeLoadingContext &context) const { KoStyleStack &styleStack = context.odfLoadingContext().styleStack(); KoOdfStylesReader &stylesReader = context.odfLoadingContext().stylesReader(); QString stroke = KoShapePrivate::getStyleProperty("stroke", context); if (stroke == "solid" || stroke == "dash") { QPen pen = KoOdfGraphicStyles::loadOdfStrokeStyle(styleStack, stroke, stylesReader); QSharedPointer stroke(new KoShapeStroke()); if (styleStack.hasProperty(KoXmlNS::calligra, "stroke-gradient")) { QString gradientName = styleStack.property(KoXmlNS::calligra, "stroke-gradient"); QBrush brush = KoOdfGraphicStyles::loadOdfGradientStyleByName(stylesReader, gradientName, size()); stroke->setLineBrush(brush); } else { stroke->setColor(pen.color()); } #ifndef NWORKAROUND_ODF_BUGS KoOdfWorkaround::fixPenWidth(pen, context); #endif stroke->setLineWidth(pen.widthF()); stroke->setJoinStyle(pen.joinStyle()); stroke->setLineStyle(pen.style(), pen.dashPattern()); stroke->setCapStyle(pen.capStyle()); return stroke; #ifndef NWORKAROUND_ODF_BUGS } else if (stroke.isEmpty()) { QPen pen = KoOdfGraphicStyles::loadOdfStrokeStyle(styleStack, "solid", stylesReader); if (KoOdfWorkaround::fixMissingStroke(pen, element, context, this)) { QSharedPointer stroke(new KoShapeStroke()); #ifndef NWORKAROUND_ODF_BUGS KoOdfWorkaround::fixPenWidth(pen, context); #endif stroke->setLineWidth(pen.widthF()); stroke->setJoinStyle(pen.joinStyle()); stroke->setLineStyle(pen.style(), pen.dashPattern()); stroke->setCapStyle(pen.capStyle()); stroke->setColor(pen.color()); return stroke; } #endif } return KoShapeStrokeModelSP(); } KoShapeShadow *KoShapePrivate::loadOdfShadow(KoShapeLoadingContext &context) const { KoStyleStack &styleStack = context.odfLoadingContext().styleStack(); QString shadowStyle = KoShapePrivate::getStyleProperty("shadow", context); if (shadowStyle == "visible" || shadowStyle == "hidden") { KoShapeShadow *shadow = new KoShapeShadow(); QColor shadowColor(styleStack.property(KoXmlNS::draw, "shadow-color")); qreal offsetX = KoUnit::parseValue(styleStack.property(KoXmlNS::draw, "shadow-offset-x")); qreal offsetY = KoUnit::parseValue(styleStack.property(KoXmlNS::draw, "shadow-offset-y")); shadow->setOffset(QPointF(offsetX, offsetY)); qreal blur = KoUnit::parseValue(styleStack.property(KoXmlNS::calligra, "shadow-blur-radius")); shadow->setBlur(blur); QString opacity = styleStack.property(KoXmlNS::draw, "shadow-opacity"); if (! opacity.isEmpty() && opacity.right(1) == "%") shadowColor.setAlphaF(opacity.left(opacity.length() - 1).toFloat() / 100.0); shadow->setColor(shadowColor); shadow->setVisible(shadowStyle == "visible"); return shadow; } return 0; } KoBorder *KoShapePrivate::loadOdfBorder(KoShapeLoadingContext &context) const { KoStyleStack &styleStack = context.odfLoadingContext().styleStack(); KoBorder *border = new KoBorder(); if (border->loadOdf(styleStack)) { return border; } delete border; return 0; } void KoShape::loadOdfGluePoints(const KoXmlElement &element, KoShapeLoadingContext &context) { Q_D(KoShape); KoXmlElement child; bool hasCenterGluePoint = false; forEachElement(child, element) { if (child.namespaceURI() != KoXmlNS::draw) continue; if (child.localName() != "glue-point") continue; // NOTE: this uses draw:id, but apparently while ODF 1.2 has deprecated // all use of draw:id for xml:id, it didn't specify that here, so it // doesn't support xml:id (and so, maybe, shouldn't use KoElementReference. const QString id = child.attributeNS(KoXmlNS::draw, "id", QString()); const int index = id.toInt(); // connection point in center should be default but odf doesn't support, // in new shape, first custom point is in center, it's okay to replace that point // with point from xml now, we'll add it back later if(id.isEmpty() || index < KoConnectionPoint::FirstCustomConnectionPoint || (index != KoConnectionPoint::FirstCustomConnectionPoint && d->connectors.contains(index))) { warnFlake << "glue-point with no or invalid id"; continue; } QString xStr = child.attributeNS(KoXmlNS::svg, "x", QString()).simplified(); QString yStr = child.attributeNS(KoXmlNS::svg, "y", QString()).simplified(); if(xStr.isEmpty() || yStr.isEmpty()) { warnFlake << "glue-point with invald position"; continue; } KoConnectionPoint connector; const QString align = child.attributeNS(KoXmlNS::draw, "align", QString()); if (align.isEmpty()) { #ifndef NWORKAROUND_ODF_BUGS KoOdfWorkaround::fixGluePointPosition(xStr, context); KoOdfWorkaround::fixGluePointPosition(yStr, context); #endif if(!xStr.endsWith('%') || !yStr.endsWith('%')) { warnFlake << "glue-point with invald position"; continue; } // x and y are relative to drawing object center connector.position.setX(xStr.remove('%').toDouble()/100.0); connector.position.setY(yStr.remove('%').toDouble()/100.0); // convert position to be relative to top-left corner connector.position += QPointF(0.5, 0.5); connector.position.rx() = qBound(0.0, connector.position.x(), 1.0); connector.position.ry() = qBound(0.0, connector.position.y(), 1.0); } else { // absolute distances to the edge specified by align connector.position.setX(KoUnit::parseValue(xStr)); connector.position.setY(KoUnit::parseValue(yStr)); if (align == "top-left") { connector.alignment = KoConnectionPoint::AlignTopLeft; } else if (align == "top") { connector.alignment = KoConnectionPoint::AlignTop; } else if (align == "top-right") { connector.alignment = KoConnectionPoint::AlignTopRight; } else if (align == "left") { connector.alignment = KoConnectionPoint::AlignLeft; } else if (align == "center") { connector.alignment = KoConnectionPoint::AlignCenter; } else if (align == "right") { connector.alignment = KoConnectionPoint::AlignRight; } else if (align == "bottom-left") { connector.alignment = KoConnectionPoint::AlignBottomLeft; } else if (align == "bottom") { connector.alignment = KoConnectionPoint::AlignBottom; } else if (align == "bottom-right") { connector.alignment = KoConnectionPoint::AlignBottomRight; } debugFlake << "using alignment" << align; } const QString escape = child.attributeNS(KoXmlNS::draw, "escape-direction", QString()); if (!escape.isEmpty()) { if (escape == "horizontal") { connector.escapeDirection = KoConnectionPoint::HorizontalDirections; } else if (escape == "vertical") { connector.escapeDirection = KoConnectionPoint::VerticalDirections; } else if (escape == "left") { connector.escapeDirection = KoConnectionPoint::LeftDirection; } else if (escape == "right") { connector.escapeDirection = KoConnectionPoint::RightDirection; } else if (escape == "up") { connector.escapeDirection = KoConnectionPoint::UpDirection; } else if (escape == "down") { connector.escapeDirection = KoConnectionPoint::DownDirection; } debugFlake << "using escape direction" << escape; } d->connectors[index] = connector; debugFlake << "loaded glue-point" << index << "at position" << connector.position; if (d->connectors[index].position == QPointF(0.5, 0.5)) { hasCenterGluePoint = true; debugFlake << "center glue-point found at id " << index; } } if (!hasCenterGluePoint) { d->connectors[d->connectors.count()] = KoConnectionPoint(QPointF(0.5, 0.5), KoConnectionPoint::AllDirections, KoConnectionPoint::AlignCenter); } debugFlake << "shape has now" << d->connectors.count() << "glue-points"; } void KoShape::loadOdfClipContour(const KoXmlElement &element, KoShapeLoadingContext &context, const QSizeF &scaleFactor) { Q_D(KoShape); KoXmlElement child; forEachElement(child, element) { if (child.namespaceURI() != KoXmlNS::draw) continue; if (child.localName() != "contour-polygon") continue; debugFlake << "shape loads contour-polygon"; KoPathShape *ps = new KoPathShape(); ps->loadContourOdf(child, context, scaleFactor); ps->setTransformation(transformation()); KoClipPath *clipPath = new KoClipPath({ps}, KoFlake::UserSpaceOnUse); d->clipPath.reset(clipPath); } } QTransform KoShape::parseOdfTransform(const QString &transform) { QTransform matrix; // Split string for handling 1 transform statement at a time QStringList subtransforms = transform.split(')', QString::SkipEmptyParts); QStringList::ConstIterator it = subtransforms.constBegin(); QStringList::ConstIterator end = subtransforms.constEnd(); for (; it != end; ++it) { QStringList subtransform = (*it).split('(', QString::SkipEmptyParts); subtransform[0] = subtransform[0].trimmed().toLower(); subtransform[1] = subtransform[1].simplified(); QRegExp reg("[,( ]"); QStringList params = subtransform[1].split(reg, QString::SkipEmptyParts); if (subtransform[0].startsWith(';') || subtransform[0].startsWith(',')) subtransform[0] = subtransform[0].right(subtransform[0].length() - 1); QString cmd = subtransform[0].toLower(); if (cmd == "rotate") { QTransform rotMatrix; if (params.count() == 3) { qreal x = KoUnit::parseValue(params[1]); qreal y = KoUnit::parseValue(params[2]); rotMatrix.translate(x, y); // oo2 rotates by radians rotMatrix.rotate(-params[0].toDouble()*180.0 / M_PI); rotMatrix.translate(-x, -y); } else { // oo2 rotates by radians rotMatrix.rotate(-params[0].toDouble()*180.0 / M_PI); } matrix = matrix * rotMatrix; } else if (cmd == "translate") { QTransform moveMatrix; if (params.count() == 2) { qreal x = KoUnit::parseValue(params[0]); qreal y = KoUnit::parseValue(params[1]); moveMatrix.translate(x, y); } else // Spec : if only one param given, assume 2nd param to be 0 moveMatrix.translate(KoUnit::parseValue(params[0]) , 0); matrix = matrix * moveMatrix; } else if (cmd == "scale") { QTransform scaleMatrix; if (params.count() == 2) scaleMatrix.scale(params[0].toDouble(), params[1].toDouble()); else // Spec : if only one param given, assume uniform scaling scaleMatrix.scale(params[0].toDouble(), params[0].toDouble()); matrix = matrix * scaleMatrix; } else if (cmd == "skewx") { QPointF p = absolutePosition(KoFlake::TopLeft); QTransform shearMatrix; shearMatrix.translate(p.x(), p.y()); shearMatrix.shear(tan(-params[0].toDouble()), 0.0F); shearMatrix.translate(-p.x(), -p.y()); matrix = matrix * shearMatrix; } else if (cmd == "skewy") { QPointF p = absolutePosition(KoFlake::TopLeft); QTransform shearMatrix; shearMatrix.translate(p.x(), p.y()); shearMatrix.shear(0.0F, tan(-params[0].toDouble())); shearMatrix.translate(-p.x(), -p.y()); matrix = matrix * shearMatrix; } else if (cmd == "matrix") { QTransform m; if (params.count() >= 6) { m.setMatrix(params[0].toDouble(), params[1].toDouble(), 0, params[2].toDouble(), params[3].toDouble(), 0, KoUnit::parseValue(params[4]), KoUnit::parseValue(params[5]), 1); } matrix = matrix * m; } } return matrix; } void KoShape::saveOdfAttributes(KoShapeSavingContext &context, int attributes) const { Q_D(const KoShape); if (attributes & OdfStyle) { KoGenStyle style; // all items that should be written to 'draw:frame' and any other 'draw:' object that inherits this shape if (context.isSet(KoShapeSavingContext::PresentationShape)) { style = KoGenStyle(KoGenStyle::PresentationAutoStyle, "presentation"); context.xmlWriter().addAttribute("presentation:style-name", saveStyle(style, context)); } else { style = KoGenStyle(KoGenStyle::GraphicAutoStyle, "graphic"); context.xmlWriter().addAttribute("draw:style-name", saveStyle(style, context)); } } if (attributes & OdfId) { if (context.isSet(KoShapeSavingContext::DrawId)) { KoElementReference ref = context.xmlid(this, "shape", KoElementReference::Counter); ref.saveOdf(&context.xmlWriter(), KoElementReference::DrawId); } } if (attributes & OdfName) { if (! name().isEmpty()) context.xmlWriter().addAttribute("draw:name", name()); } if (attributes & OdfLayer) { KoShape *parent = d->parent; while (parent) { if (dynamic_cast(parent)) { context.xmlWriter().addAttribute("draw:layer", parent->name()); break; } parent = parent->parent(); } } if (attributes & OdfZIndex && context.isSet(KoShapeSavingContext::ZIndex)) { context.xmlWriter().addAttribute("draw:z-index", zIndex()); } if (attributes & OdfSize) { QSizeF s(size()); if (parent() && parent()->isClipped(this)) { // being clipped shrinks our visible size // clipping in ODF is done using a combination of visual size and content cliprect. // A picture of 10cm x 10cm displayed in a box of 2cm x 4cm will be scaled (out // of proportion in this case). If we then add a fo:clip like; // fo:clip="rect(2cm, 3cm, 4cm, 5cm)" (top, right, bottom, left) // our original 10x10 is clipped to 2cm x 4cm and *then* fitted in that box. // TODO do this properly by subtracting rects s = parent()->size(); } context.xmlWriter().addAttribute("svg:width", s.width()); context.xmlWriter().addAttribute("svg:height", s.height()); } // The position is implicitly stored in the transformation matrix // if the transformation is saved as well if ((attributes & OdfPosition) && !(attributes & OdfTransformation)) { const QPointF p(position() * context.shapeOffset(this)); context.xmlWriter().addAttribute("svg:x", p.x()); context.xmlWriter().addAttribute("svg:y", p.y()); } if (attributes & OdfTransformation) { QTransform matrix = absoluteTransformation(0) * context.shapeOffset(this); if (! matrix.isIdentity()) { if (qAbs(matrix.m11() - 1) < 1E-5 // 1 && qAbs(matrix.m12()) < 1E-5 // 0 && qAbs(matrix.m21()) < 1E-5 // 0 && qAbs(matrix.m22() - 1) < 1E-5) { // 1 context.xmlWriter().addAttribute("svg:x", matrix.dx()); context.xmlWriter().addAttribute("svg:y", matrix.dy()); } else { QString m = QString("matrix(%1 %2 %3 %4 %5pt %6pt)") .arg(matrix.m11(), 0, 'f', 11) .arg(matrix.m12(), 0, 'f', 11) .arg(matrix.m21(), 0, 'f', 11) .arg(matrix.m22(), 0, 'f', 11) .arg(matrix.dx(), 0, 'f', 11) .arg(matrix.dy(), 0, 'f', 11); context.xmlWriter().addAttribute("draw:transform", m); } } } if (attributes & OdfViewbox) { const QSizeF s(size()); QString viewBox = QString("0 0 %1 %2").arg(qRound(s.width())).arg(qRound(s.height())); context.xmlWriter().addAttribute("svg:viewBox", viewBox); } if (attributes & OdfAdditionalAttributes) { QMap::const_iterator it(d->additionalAttributes.constBegin()); for (; it != d->additionalAttributes.constEnd(); ++it) { context.xmlWriter().addAttribute(it.key().toUtf8(), it.value()); } } } void KoShape::saveOdfCommonChildElements(KoShapeSavingContext &context) const { Q_D(const KoShape); // save glue points see ODF 9.2.19 Glue Points if(d->connectors.count()) { KoConnectionPoints::const_iterator cp = d->connectors.constBegin(); KoConnectionPoints::const_iterator lastCp = d->connectors.constEnd(); for(; cp != lastCp; ++cp) { // do not save default glue points if(cp.key() < 4) continue; context.xmlWriter().startElement("draw:glue-point"); context.xmlWriter().addAttribute("draw:id", QString("%1").arg(cp.key())); if (cp.value().alignment == KoConnectionPoint::AlignNone) { // convert to percent from center const qreal x = cp.value().position.x() * 100.0 - 50.0; const qreal y = cp.value().position.y() * 100.0 - 50.0; context.xmlWriter().addAttribute("svg:x", QString("%1%").arg(x)); context.xmlWriter().addAttribute("svg:y", QString("%1%").arg(y)); } else { context.xmlWriter().addAttribute("svg:x", cp.value().position.x()); context.xmlWriter().addAttribute("svg:y", cp.value().position.y()); } QString escapeDirection; switch(cp.value().escapeDirection) { case KoConnectionPoint::HorizontalDirections: escapeDirection = "horizontal"; break; case KoConnectionPoint::VerticalDirections: escapeDirection = "vertical"; break; case KoConnectionPoint::LeftDirection: escapeDirection = "left"; break; case KoConnectionPoint::RightDirection: escapeDirection = "right"; break; case KoConnectionPoint::UpDirection: escapeDirection = "up"; break; case KoConnectionPoint::DownDirection: escapeDirection = "down"; break; default: // fall through break; } if(!escapeDirection.isEmpty()) { context.xmlWriter().addAttribute("draw:escape-direction", escapeDirection); } QString alignment; switch(cp.value().alignment) { case KoConnectionPoint::AlignTopLeft: alignment = "top-left"; break; case KoConnectionPoint::AlignTop: alignment = "top"; break; case KoConnectionPoint::AlignTopRight: alignment = "top-right"; break; case KoConnectionPoint::AlignLeft: alignment = "left"; break; case KoConnectionPoint::AlignCenter: alignment = "center"; break; case KoConnectionPoint::AlignRight: alignment = "right"; break; case KoConnectionPoint::AlignBottomLeft: alignment = "bottom-left"; break; case KoConnectionPoint::AlignBottom: alignment = "bottom"; break; case KoConnectionPoint::AlignBottomRight: alignment = "bottom-right"; break; default: // fall through break; } if(!alignment.isEmpty()) { context.xmlWriter().addAttribute("draw:align", alignment); } context.xmlWriter().endElement(); } } } void KoShape::saveOdfClipContour(KoShapeSavingContext &context, const QSizeF &originalSize) const { Q_D(const KoShape); debugFlake << "shape saves contour-polygon"; if (d->clipPath && !d->clipPath->clipPathShapes().isEmpty()) { // This will loose data as odf can only save one set of contour whereas // svg loading and at least karbon editing can produce more than one // TODO, FIXME see if we can save more than one clipshape to odf d->clipPath->clipPathShapes().first()->saveContourOdf(context, originalSize); } } // end loading & saving methods // static void KoShape::applyConversion(QPainter &painter, const KoViewConverter &converter) { qreal zoomX, zoomY; converter.zoom(&zoomX, &zoomY); painter.scale(zoomX, zoomY); } KisHandlePainterHelper KoShape::createHandlePainterHelper(QPainter *painter, KoShape *shape, const KoViewConverter &converter, qreal handleRadius) { const QTransform originalPainterTransform = painter->transform(); painter->setTransform(shape->absoluteTransformation(&converter) * painter->transform()); KoShape::applyConversion(*painter, converter); // move c-tor return KisHandlePainterHelper(painter, originalPainterTransform, handleRadius); } QPointF KoShape::shapeToDocument(const QPointF &point) const { return absoluteTransformation(0).map(point); } QRectF KoShape::shapeToDocument(const QRectF &rect) const { return absoluteTransformation(0).mapRect(rect); } QPointF KoShape::documentToShape(const QPointF &point) const { return absoluteTransformation(0).inverted().map(point); } QRectF KoShape::documentToShape(const QRectF &rect) const { return absoluteTransformation(0).inverted().mapRect(rect); } bool KoShape::addDependee(KoShape *shape) { Q_D(KoShape); if (! shape) return false; // refuse to establish a circular dependency if (shape->hasDependee(this)) return false; if (! d->dependees.contains(shape)) d->dependees.append(shape); return true; } void KoShape::removeDependee(KoShape *shape) { Q_D(KoShape); int index = d->dependees.indexOf(shape); if (index >= 0) d->dependees.removeAt(index); } bool KoShape::hasDependee(KoShape *shape) const { Q_D(const KoShape); return d->dependees.contains(shape); } QList KoShape::dependees() const { Q_D(const KoShape); return d->dependees; } void KoShape::shapeChanged(ChangeType type, KoShape *shape) { Q_UNUSED(type); Q_UNUSED(shape); } KoSnapData KoShape::snapData() const { return KoSnapData(); } void KoShape::setAdditionalAttribute(const QString &name, const QString &value) { Q_D(KoShape); d->additionalAttributes.insert(name, value); } void KoShape::removeAdditionalAttribute(const QString &name) { Q_D(KoShape); d->additionalAttributes.remove(name); } bool KoShape::hasAdditionalAttribute(const QString &name) const { Q_D(const KoShape); return d->additionalAttributes.contains(name); } QString KoShape::additionalAttribute(const QString &name) const { Q_D(const KoShape); return d->additionalAttributes.value(name); } void KoShape::setAdditionalStyleAttribute(const char *name, const QString &value) { Q_D(KoShape); d->additionalStyleAttributes.insert(name, value); } void KoShape::removeAdditionalStyleAttribute(const char *name) { Q_D(KoShape); d->additionalStyleAttributes.remove(name); } KoFilterEffectStack *KoShape::filterEffectStack() const { Q_D(const KoShape); return d->filterEffectStack; } void KoShape::setFilterEffectStack(KoFilterEffectStack *filterEffectStack) { Q_D(KoShape); if (d->filterEffectStack) d->filterEffectStack->deref(); d->filterEffectStack = filterEffectStack; if (d->filterEffectStack) { d->filterEffectStack->ref(); } notifyChanged(); } QSet KoShape::toolDelegates() const { Q_D(const KoShape); return d->toolDelegates; } void KoShape::setToolDelegates(const QSet &delegates) { Q_D(KoShape); d->toolDelegates = delegates; } QString KoShape::hyperLink () const { Q_D(const KoShape); return d->hyperLink; } void KoShape::setHyperLink(const QString &hyperLink) { Q_D(KoShape); d->hyperLink = hyperLink; } KoShapePrivate *KoShape::priv() { Q_D(KoShape); return d; } KoShape::ShapeChangeListener::~ShapeChangeListener() { Q_FOREACH(KoShape *shape, m_registeredShapes) { shape->removeShapeChangeListener(this); } } void KoShape::ShapeChangeListener::registerShape(KoShape *shape) { KIS_SAFE_ASSERT_RECOVER_RETURN(!m_registeredShapes.contains(shape)); m_registeredShapes.append(shape); } void KoShape::ShapeChangeListener::unregisterShape(KoShape *shape) { KIS_SAFE_ASSERT_RECOVER_RETURN(m_registeredShapes.contains(shape)); m_registeredShapes.removeAll(shape); } void KoShape::ShapeChangeListener::notifyShapeChangedImpl(KoShape::ChangeType type, KoShape *shape) { KIS_SAFE_ASSERT_RECOVER_RETURN(m_registeredShapes.contains(shape)); notifyShapeChanged(type, shape); if (type == KoShape::Deleted) { unregisterShape(shape); } } void KoShape::addShapeChangeListener(KoShape::ShapeChangeListener *listener) { Q_D(KoShape); KIS_SAFE_ASSERT_RECOVER_RETURN(!d->listeners.contains(listener)); listener->registerShape(this); d->listeners.append(listener); } void KoShape::removeShapeChangeListener(KoShape::ShapeChangeListener *listener) { Q_D(KoShape); KIS_SAFE_ASSERT_RECOVER_RETURN(d->listeners.contains(listener)); d->listeners.removeAll(listener); listener->unregisterShape(this); } QList KoShape::linearizeSubtree(const QList &shapes) { QList result; Q_FOREACH (KoShape *shape, shapes) { result << shape; KoShapeContainer *container = dynamic_cast(shape); if (container) { result << linearizeSubtree(container->shapes()); } } return result; } diff --git a/libs/flake/KoShapeSavingContext.cpp b/libs/flake/KoShapeSavingContext.cpp index 46d1d29acd..d7d2aa50a8 100644 --- a/libs/flake/KoShapeSavingContext.cpp +++ b/libs/flake/KoShapeSavingContext.cpp @@ -1,346 +1,346 @@ /* This file is part of the KDE project Copyright (C) 2004-2006 David Faure Copyright (C) 2007-2009, 2011 Thorsten Zachmann Copyright (C) 2007 Jan Hambrecht Copyright (C) 2010 Benjamin Port This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoShapeSavingContext.h" #include "KoDataCenterBase.h" #include "KoShapeLayer.h" #include "KoImageData.h" #include "KoMarker.h" #include #include #include #include #include #include #include #include #include class KoShapeSavingContextPrivate { public: KoShapeSavingContextPrivate(KoXmlWriter&, KoGenStyles&, KoEmbeddedDocumentSaver&); ~KoShapeSavingContextPrivate(); KoXmlWriter *xmlWriter; KoShapeSavingContext::ShapeSavingOptions savingOptions; QList layers; QSet dataCenters; QMap sharedData; QMap imageNames; int imageId; QMap images; QHash shapeOffsets; QMap markerRefs; KoGenStyles& mainStyles; KoEmbeddedDocumentSaver& embeddedSaver; QMap references; QMap referenceCounters; QMap > prefixedReferences; }; KoShapeSavingContextPrivate::KoShapeSavingContextPrivate(KoXmlWriter &w, KoGenStyles &s, KoEmbeddedDocumentSaver &e) : xmlWriter(&w), savingOptions(0), imageId(0), mainStyles(s), embeddedSaver(e) { } KoShapeSavingContextPrivate::~KoShapeSavingContextPrivate() { Q_FOREACH (KoSharedSavingData * data, sharedData) { delete data; } } KoShapeSavingContext::KoShapeSavingContext(KoXmlWriter &xmlWriter, KoGenStyles &mainStyles, KoEmbeddedDocumentSaver &embeddedSaver) : d(new KoShapeSavingContextPrivate(xmlWriter, mainStyles, embeddedSaver)) { // by default allow saving of draw:id + xml:id addOption(KoShapeSavingContext::DrawId); } KoShapeSavingContext::~KoShapeSavingContext() { delete d; } KoXmlWriter & KoShapeSavingContext::xmlWriter() { return *d->xmlWriter; } void KoShapeSavingContext::setXmlWriter(KoXmlWriter &xmlWriter) { d->xmlWriter = &xmlWriter; } KoGenStyles & KoShapeSavingContext::mainStyles() { return d->mainStyles; } KoEmbeddedDocumentSaver &KoShapeSavingContext::embeddedSaver() { return d->embeddedSaver; } bool KoShapeSavingContext::isSet(ShapeSavingOption option) const { return d->savingOptions & option; } void KoShapeSavingContext::setOptions(ShapeSavingOptions options) { d->savingOptions = options; } KoShapeSavingContext::ShapeSavingOptions KoShapeSavingContext::options() const { return d->savingOptions; } void KoShapeSavingContext::addOption(ShapeSavingOption option) { d->savingOptions = d->savingOptions | option; } void KoShapeSavingContext::removeOption(ShapeSavingOption option) { if (isSet(option)) d->savingOptions = d->savingOptions ^ option; // xor to remove it. } KoElementReference KoShapeSavingContext::xmlid(const void *referent, const QString& prefix, KoElementReference::GenerationOption counter) { Q_ASSERT(counter == KoElementReference::UUID || (counter == KoElementReference::Counter && !prefix.isEmpty())); if (d->references.contains(referent)) { return d->references[referent]; } KoElementReference ref; if (counter == KoElementReference::Counter) { int referenceCounter = d->referenceCounters[prefix]; referenceCounter++; ref = KoElementReference(prefix, referenceCounter); d->references.insert(referent, ref); d->referenceCounters[prefix] = referenceCounter; } else { if (!prefix.isEmpty()) { ref = KoElementReference(prefix); d->references.insert(referent, ref); } else { d->references.insert(referent, ref); } } if (!prefix.isNull()) { d->prefixedReferences[prefix].append(referent); } return ref; } KoElementReference KoShapeSavingContext::existingXmlid(const void *referent) { if (d->references.contains(referent)) { return d->references[referent]; } else { KoElementReference ref; ref.invalidate(); return ref; } } void KoShapeSavingContext::clearXmlIds(const QString &prefix) { if (d->prefixedReferences.contains(prefix)) { Q_FOREACH (const void* ptr, d->prefixedReferences[prefix]) { d->references.remove(ptr); } d->prefixedReferences.remove(prefix); } if (d->referenceCounters.contains(prefix)) { d->referenceCounters[prefix] = 0; } } void KoShapeSavingContext::addLayerForSaving(const KoShapeLayer *layer) { if (layer && ! d->layers.contains(layer)) d->layers.append(layer); } void KoShapeSavingContext::saveLayerSet(KoXmlWriter &xmlWriter) const { xmlWriter.startElement("draw:layer-set"); Q_FOREACH (const KoShapeLayer * layer, d->layers) { xmlWriter.startElement("draw:layer"); xmlWriter.addAttribute("draw:name", layer->name()); if (layer->isGeometryProtected()) xmlWriter.addAttribute("draw:protected", "true"); if (! layer->isVisible(false)) xmlWriter.addAttribute("draw:display", "none"); xmlWriter.endElement(); // draw:layer } xmlWriter.endElement(); // draw:layer-set } void KoShapeSavingContext::clearLayers() { d->layers.clear(); } QString KoShapeSavingContext::imageHref(const KoImageData *image) { QMap::iterator it(d->imageNames.find(image->key())); if (it == d->imageNames.end()) { QString suffix = image->suffix(); if (suffix.isEmpty()) { it = d->imageNames.insert(image->key(), QString("Pictures/image%1").arg(++d->imageId)); } else { it = d->imageNames.insert(image->key(), QString("Pictures/image%1.%2").arg(++d->imageId).arg(suffix)); } } return it.value(); } QString KoShapeSavingContext::imageHref(const QImage &image) { // TODO this can be optimized to recognize images which have the same content - // Also this can use quite a lot of memeory as the qimage are all kept until + // Also this can use quite a lot of memory as the qimage are all kept until // they are saved to the store in memory QString href = QString("Pictures/image%1.png").arg(++d->imageId); d->images.insert(href, image); return href; } QMap KoShapeSavingContext::imagesToSave() { return d->imageNames; } QString KoShapeSavingContext::markerRef(const KoMarker */*marker*/) { // QMap::iterator it = d->markerRefs.find(marker); // if (it == d->markerRefs.end()) { // it = d->markerRefs.insert(marker, marker->saveOdf(*this)); // } // return it.value(); return QString(); } void KoShapeSavingContext::addDataCenter(KoDataCenterBase * dataCenter) { if (dataCenter) { d->dataCenters.insert(dataCenter); } } bool KoShapeSavingContext::saveDataCenter(KoStore *store, KoXmlWriter* manifestWriter) { bool ok = true; Q_FOREACH (KoDataCenterBase *dataCenter, d->dataCenters) { ok = ok && dataCenter->completeSaving(store, manifestWriter, this); //debugFlake << "ok" << ok; } // Save images for (QMap::iterator it(d->images.begin()); it != d->images.end(); ++it) { if (store->open(it.key())) { KoStoreDevice device(store); ok = ok && it.value().save(&device, "PNG"); store->close(); // TODO error handling if (ok) { const QString mimetype = KisMimeDatabase::mimeTypeForFile(it.key(), false); manifestWriter->addManifestEntry(it.key(), mimetype); } else { warnFlake << "saving image failed"; } } else { ok = false; warnFlake << "saving image failed: open store failed"; } } return ok; } void KoShapeSavingContext::addSharedData(const QString &id, KoSharedSavingData * data) { QMap::iterator it(d->sharedData.find(id)); // data will not be overwritten if (it == d->sharedData.end()) { d->sharedData.insert(id, data); } else { warnFlake << "The id" << id << "is already registered. Data not inserted"; Q_ASSERT(it == d->sharedData.end()); } } KoSharedSavingData * KoShapeSavingContext::sharedData(const QString &id) const { KoSharedSavingData * data = 0; QMap::const_iterator it(d->sharedData.constFind(id)); if (it != d->sharedData.constEnd()) { data = it.value(); } return data; } void KoShapeSavingContext::addShapeOffset(const KoShape *shape, const QTransform &m) { d->shapeOffsets.insert(shape, m); } void KoShapeSavingContext::removeShapeOffset(const KoShape *shape) { d->shapeOffsets.remove(shape); } QTransform KoShapeSavingContext::shapeOffset(const KoShape *shape) const { return d->shapeOffsets.value(shape, QTransform()); } diff --git a/libs/flake/KoToolBase.h b/libs/flake/KoToolBase.h index df62ab0f30..5214ea8249 100644 --- a/libs/flake/KoToolBase.h +++ b/libs/flake/KoToolBase.h @@ -1,544 +1,544 @@ /* This file is part of the KDE project * Copyright (C) 2006 Thomas Zander * Copyright (C) 2011 Jan Hambrecht * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOTOOLBASE_H #define KOTOOLBASE_H #include #include #include #include #include #include "kritaflake_export.h" class KoShape; class KoCanvasBase; class KoPointerEvent; class KoViewConverter; class KoToolSelection; class KoToolBasePrivate; class KoShapeControllerBase; class QAction; class QKeyEvent; class QWidget; class QCursor; class QPainter; class QString; class QStringList; class QRectF; class QPointF; class QInputMethodEvent; class QDragMoveEvent; class QDragLeaveEvent; class QDropEvent; class QTouchEvent; class QMenu; /** * Abstract base class for all tools. Tools can create or manipulate * flake shapes, canvas state or any other thing that a user may wish * to do to his document or his view on a document with a pointing * device. * * There exists an instance of every tool for every pointer device. * These instances are managed by the toolmanager.. */ class KRITAFLAKE_EXPORT KoToolBase : public QObject { Q_OBJECT public: /// Option for activate() enum ToolActivation { TemporaryActivation, ///< The tool is activated temporarily and works 'in-place' of another one. DefaultActivation ///< The tool is activated normally and emitting 'done' goes to the defaultTool }; /** * Constructor, normally only called by the factory (see KoToolFactoryBase) * @param canvas the canvas interface this tool will work for. */ explicit KoToolBase(KoCanvasBase *canvas); ~KoToolBase() override; /** * request a repaint of the decorations to be made. This triggers * an update call on the canvas, but does not paint directly. */ virtual void repaintDecorations(); /** * Return if dragging (moving with the mouse down) to the edge of a canvas should scroll the * canvas (default is true). * @return if this tool wants mouse events to cause scrolling of canvas. */ virtual bool wantsAutoScroll() const; /** * Called by the canvas to paint any decorations that the tool deems needed. * The painter has the top left of the canvas as its origin. * @param painter used for painting the shape * @param converter to convert between internal and view coordinates. */ virtual void paint(QPainter &painter, const KoViewConverter &converter) = 0; /** * Return the option widgets for this tool. Create them if they * do not exist yet. If the tool does not have an option widget, * this method return an empty list. (After discussion with Thomas, who prefers * the toolmanager to handle that case.) * * @see m_optionWidgets */ QList > optionWidgets(); /** * Retrieves the entire collection of actions for the tool. */ QHash actions() const; /** * Retrieve an action by name. */ QAction *action(const QString &name) const; /** * Called when (one of) the mouse or stylus buttons is pressed. * Implementors should call event->ignore() if they do not actually use the event. * @param event state and reason of this mouse or stylus press */ virtual void mousePressEvent(KoPointerEvent *event) = 0; /** * Called when (one of) the mouse or stylus buttons is double clicked. * Implementors should call event->ignore() if they do not actually use the event. * Default implementation ignores this event. * @param event state and reason of this mouse or stylus press */ virtual void mouseDoubleClickEvent(KoPointerEvent *event); /** * Called when (one of) the mouse or stylus buttons is triple clicked. * Implementors should call event->ignore() if they do not actually use the event. * Default implementation ignores this event. * @param event state and reason of this mouse or stylus press */ virtual void mouseTripleClickEvent(KoPointerEvent *event); /** * Called when the mouse or stylus moved over the canvas. * Implementors should call event->ignore() if they do not actually use the event. * @param event state and reason of this mouse or stylus move */ virtual void mouseMoveEvent(KoPointerEvent *event) = 0; /** * Called when (one of) the mouse or stylus buttons is released. * Implementors should call event->ignore() if they do not actually use the event. * @param event state and reason of this mouse or stylus release */ virtual void mouseReleaseEvent(KoPointerEvent *event) = 0; /** * Called when a key is pressed. * Implementors should call event->ignore() if they do not actually use the event. * Default implementation ignores this event. * @param event state and reason of this key press */ virtual void keyPressEvent(QKeyEvent *event); /** * Called when a key is released * Implementors should call event->ignore() if they do not actually use the event. * Default implementation ignores this event. * @param event state and reason of this key release */ virtual void keyReleaseEvent(QKeyEvent *event); /** * @brief explicitUserStrokeEndRequest is called by the input manager * when the user presses Enter key or any equivalent. This callback * comes before requestStrokeEnd(), which comes from a different source. */ virtual void explicitUserStrokeEndRequest(); /** * This method is used to query a set of properties of the tool to be * able to support complex input method operations as support for surrounding * text and reconversions. * Default implementation returns simple defaults, for tools that want to provide * a more responsive text entry experience for CJK languages it would be good to reimplemnt. * @param query specifies which property is queried. * @param converter the view converter for the current canvas. */ virtual QVariant inputMethodQuery(Qt::InputMethodQuery query, const KoViewConverter &converter) const; /** * Text entry of complex text, like CJK, can be made more interactive if a tool * implements this and the InputMethodQuery() methods. * Reimplementing this only provides the user with a more responsive text experience, since the * default implementation forwards the typed text as key pressed events. * @param event the input method event. */ virtual void inputMethodEvent(QInputMethodEvent *event); /** * Called when (one of) a custom device buttons is pressed. * Implementors should call event->ignore() if they do not actually use the event. * @param event state and reason of this custom device press */ virtual void customPressEvent(KoPointerEvent *event); /** * Called when (one of) a custom device buttons is released. * Implementors should call event->ignore() if they do not actually use the event. * @param event state and reason of this custom device release */ virtual void customReleaseEvent(KoPointerEvent *event); /** * Called when a custom device moved over the canvas. * Implementors should call event->ignore() if they do not actually use the event. * @param event state and reason of this custom device move */ virtual void customMoveEvent(KoPointerEvent *event); /** * @return true if synthetic mouse events on the canvas should be eaten. * * For example, the guides tool should allow click and drag with touch, * while the same touch events should be rejected by the freehand tool. * * These events are sent by the OS in Windows */ bool maskSyntheticEvents() const; /** * get the identifier code from the KoToolFactoryBase that created this tool. * @return the toolId. * @see KoToolFactoryBase::id() */ Q_INVOKABLE QString toolId() const; /// return the last emitted cursor QCursor cursor() const; /** * Returns the internal selection object of this tool. * Each tool can have a selection which is private to that tool and the specified shape that it comes with. * The default returns 0. */ virtual KoToolSelection *selection(); /** * @returns true if the tool has selected data. */ virtual bool hasSelection(); /** * copies the tools selection to the clipboard. * The default implementation is empty to aid tools that don't have any selection. * @see selection() */ virtual void copy() const; /** * Delete the tools selection. * The default implementation is empty to aid tools that don't have any selection. * @see selection() */ virtual void deleteSelection(); /** * Cut the tools selection and copy it to the clipboard. * The default implementation calls copy() and then deleteSelection() * @see copy() * @see deleteSelection() */ virtual void cut(); /** * Paste the clipboard selection. * A tool typically has one or more shapes selected and pasting should do something meaningful * for this specific shape and tool combination. Inserting text in a text tool, for example. * @return will return true if pasting succeeded. False if nothing happened. */ virtual bool paste(); /** * Handle the dragMoveEvent * A tool typically has one or more shapes selected and dropping into should do * something meaningful for this specific shape and tool combination. For example * dropping text in a text tool. * The tool should Accept the event if it is meaningful; Default implementation does not. */ virtual void dragMoveEvent(QDragMoveEvent *event, const QPointF &point); /** * Handle the dragLeaveEvent * Basically just a noticification that the drag is no long relevant * The tool should Accept the event if it is meaningful; Default implementation does not. */ virtual void dragLeaveEvent(QDragLeaveEvent *event); /** * Handle the dropEvent * A tool typically has one or more shapes selected and dropping into should do * something meaningful for this specific shape and tool combination. For example * dropping text in a text tool. * The tool should Accept the event if it is meaningful; Default implementation does not. */ virtual void dropEvent(QDropEvent *event, const QPointF &point); /** - * @return a menu with context-aware actions for the currect selection. If + * @return a menu with context-aware actions for the current selection. If * the returned value is null, no context menu is shown. */ virtual QMenu* popupActionsMenu(); /// Returns the canvas the tool is working on KoCanvasBase *canvas() const; /** * This method can be reimplemented in a subclass. * @return returns true, if the tool is in text mode. that means, that there is * any kind of textual input and all single key shortcuts should be eaten. */ bool isInTextMode() const; public Q_SLOTS: /** * Called when the user requested undo while the stroke is * active. If you tool supports undo of the part of its actions, * override this method and do the needed work there. * * NOTE: Default implementation forwards this request to * requestStrokeCancellation() method, so that the stroke * would be cancelled. */ virtual void requestUndoDuringStroke(); /** * Called when the user requested the cancellation of the current * stroke. If you tool supports cancelling, override this method * and do the needed work there */ virtual void requestStrokeCancellation(); /** * Called when the image decided that the stroke should better be * ended. If you tool supports long strokes, override this method * and do the needed work there */ virtual void requestStrokeEnd(); /** * This method is called when this tool instance is activated. * For any main window there is only one tool active at a time, which then gets all * user input. Switching between tools will call deactivate on one and activate on the * new tool allowing the tool to flush items (like a selection) * when it is not in use. * *

There is one case where two tools are activated at the same. This is the case * where one tool delegates work to another temporarily. For example, while shift is * being held down. The second tool will get activated with temporary=true and * it should emit done() when the state that activated it is ended. *

One of the important tasks of activate is to call useCursor() * * @param shapes the set of shapes that are selected or suggested for editing by a * selected shape for the tool to work on. Not all shapes will be meant for this * tool. * @param toolActivation if TemporaryActivation, this tool is only temporarily activated * and should emit done when it is done. * @see deactivate() */ virtual void activate(ToolActivation toolActivation, const QSet &shapes); /** * This method is called whenever this tool is no longer the * active tool * @see activate() */ virtual void deactivate(); /** * This method is called whenever a property in the resource * provider associated with the canvas this tool belongs to * changes. An example is currently selected foreground color. */ virtual void canvasResourceChanged(int key, const QVariant &res); /** * This method is called whenever a property in the resource * provider associated with the document this tool belongs to * changes. An example is the handle radius */ virtual void documentResourceChanged(int key, const QVariant &res); /** * This method just relays the given text via the tools statusTextChanged signal. * @param statusText the new status text */ void setStatusText(const QString &statusText); Q_SIGNALS: /** * Emitted when this tool wants itself to be replaced by another tool. * * @param id the identification of the desired tool * @see toolId(), KoToolFactoryBase::id() */ void activateTool(const QString &id); /** * Emitted when this tool wants itself to temporarily be replaced by another tool. * For instance, a paint tool could desire to be * temporarily replaced by a pan tool which could be temporarily * replaced by a colorpicker. * @param id the identification of the desired tool */ void activateTemporary(const QString &id); /** * Emitted when the tool has been temporarily activated and wants * to notify the world that it's done. */ void done(); /** * Emitted by useCursor() when the cursor to display on the canvas is changed. * The KoToolManager should connect to this signal to handle cursors further. */ void cursorChanged(const QCursor &cursor); /** * A tool can have a selection that is copy-able, this signal is emitted when that status changes. * @param hasSelection is true when the tool holds selected data. */ void selectionChanged(bool hasSelection); /** * Emitted when the tool wants to display a different status text * @param statusText the new status text */ void statusTextChanged(const QString &statusText); protected: /** * Classes inheriting from this one can call this method to signify which cursor * the tool wants to display at this time. Logical place to call it is after an * incoming event has been handled. * @param cursor the new cursor. */ void useCursor(const QCursor &cursor); /** * Reimplement this if your tool actually has an option widget. * Sets the option widget to 0 by default. */ virtual QWidget *createOptionWidget(); virtual QList > createOptionWidgets(); /** * Add an action under the given name to the collection. * * Inserting an action under a name that is already used for another action will replace * the other action in the collection. * * @param name The name by which the action be retrieved again from the collection. * @param action The action to add. * @param readWrite set this to ReadOnlyAction to keep the action available on * read-only documents */ void addAction(const QString &name, QAction *action); /// Convenience function to get the current handle radius uint handleRadius() const; /// Convencience function to get the current grab sensitivity uint grabSensitivity() const; /** * Returns a handle grab rect at the given position. * * The position is expected to be in document coordinates. The grab sensitivity * canvas resource is used for the dimension of the rectangle. * * @return the handle rectangle in document coordinates */ QRectF handleGrabRect(const QPointF &position) const; /** * Returns a handle paint rect at the given position. * * The position is expected to be in document coordinates. The handle radius * canvas resource is used for the dimension of the rectangle. * * @return the handle rectangle in document coordinates */ QRectF handlePaintRect(const QPointF &position) const; /** * You should set the text mode to true in subclasses, if this tool is in text input mode, eg if the users * are able to type. If you don't set it, then single key shortcuts will get the key event and not this tool. */ void setTextMode(bool value); /** * Allows subclasses to specify whether synthetic mouse events should be accepted. */ void setMaskSyntheticEvents(bool value); /** * Returns true if activate() has been called (more times than deactivate :) ) */ bool isActivated() const; protected: KoToolBase(KoToolBasePrivate &dd); KoToolBasePrivate *d_ptr; private: friend class ToolHelper; /** * Set the identifier code from the KoToolFactoryBase that created this tool. * @param id the identifier code * @see KoToolFactoryBase::id() */ void setToolId(const QString &id); KoToolBase(); KoToolBase(const KoToolBase&); KoToolBase& operator=(const KoToolBase&); Q_DECLARE_PRIVATE(KoToolBase) }; #endif /* KOTOOL_H */ diff --git a/libs/flake/commands/KoShapeAlignCommand.h b/libs/flake/commands/KoShapeAlignCommand.h index f55ac5d847..3c811a18fd 100644 --- a/libs/flake/commands/KoShapeAlignCommand.h +++ b/libs/flake/commands/KoShapeAlignCommand.h @@ -1,63 +1,63 @@ /* This file is part of the KDE project * Copyright (C) 2006 Thomas Zander * Copyright (C) 2006 Jan Hambrecht * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOSHAPEALIGNCOMMAND_H #define KOSHAPEALIGNCOMMAND_H #include "kritaflake_export.h" #include #include class KoShape; class QRectF; /// The undo / redo command for aligning shapes class KRITAFLAKE_EXPORT KoShapeAlignCommand : public KUndo2Command { public: /// The different alignment options for this command enum Align { HorizontalLeftAlignment, ///< Align left HorizontalCenterAlignment, ///< Align Centered horizontally HorizontalRightAlignment, ///< Align Right VerticalBottomAlignment, ///< Align bottom VerticalCenterAlignment, ///< Align centered vertically VerticalTopAlignment ///< Align top }; /** * Command to align a set of shapes in a rect * @param shapes a set of all the shapes that should be aligned - * @param align the aligment type + * @param align the alignment type * @param boundingRect the rect the shape will be aligned in * @param parent the parent command used for macro commands */ KoShapeAlignCommand(const QList &shapes, Align align, const QRectF &boundingRect, KUndo2Command *parent = 0); ~KoShapeAlignCommand() override; /// redo the command void redo() override; /// revert the actions done in redo void undo() override; private: class Private; Private * const d; }; #endif diff --git a/libs/flake/commands/KoShapeReorderCommand.cpp b/libs/flake/commands/KoShapeReorderCommand.cpp index e41ba4c58c..8f594270ac 100644 --- a/libs/flake/commands/KoShapeReorderCommand.cpp +++ b/libs/flake/commands/KoShapeReorderCommand.cpp @@ -1,333 +1,333 @@ /* This file is part of the KDE project * Copyright (C) 2006 Thomas Zander * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoShapeReorderCommand.h" #include "KoShape.h" #include "KoShape_p.h" #include "KoShapeManager.h" #include "KoShapeContainer.h" #include #include #include KoShapeReorderCommand::IndexedShape::IndexedShape() { } KoShapeReorderCommand::IndexedShape::IndexedShape(KoShape *_shape) : zIndex(_shape->zIndex()), shape(_shape) { } bool KoShapeReorderCommand::IndexedShape::operator<(const KoShapeReorderCommand::IndexedShape &rhs) const { return zIndex < rhs.zIndex; } class KoShapeReorderCommandPrivate { public: KoShapeReorderCommandPrivate() {} KoShapeReorderCommandPrivate(const QList &s, QList &ni) : shapes(s), newIndexes(ni) { } QList shapes; QList previousIndexes; QList newIndexes; }; KoShapeReorderCommand::KoShapeReorderCommand(const QList &shapes, QList &newIndexes, KUndo2Command *parent) : KUndo2Command(parent), d(new KoShapeReorderCommandPrivate(shapes, newIndexes)) { Q_ASSERT(shapes.count() == newIndexes.count()); foreach (KoShape *shape, shapes) d->previousIndexes.append(shape->zIndex()); setText(kundo2_i18n("Reorder shapes")); } KoShapeReorderCommand::KoShapeReorderCommand(const QList &shapes, KUndo2Command *parent) : KUndo2Command(parent), d(new KoShapeReorderCommandPrivate()) { Q_FOREACH (const IndexedShape &index, shapes) { d->shapes.append(index.shape); d->newIndexes.append(index.zIndex); d->previousIndexes.append(index.shape->zIndex()); } setText(kundo2_i18n("Reorder shapes")); } KoShapeReorderCommand::~KoShapeReorderCommand() { delete d; } void KoShapeReorderCommand::redo() { KUndo2Command::redo(); for (int i = 0; i < d->shapes.count(); i++) { - // z-index cannot chage the bounding rect of the shape, so + // z-index cannot change the bounding rect of the shape, so // no united updates needed d->shapes.at(i)->setZIndex(d->newIndexes.at(i)); d->shapes.at(i)->update(); } } void KoShapeReorderCommand::undo() { KUndo2Command::undo(); for (int i = 0; i < d->shapes.count(); i++) { - // z-index cannot chage the bounding rect of the shape, so + // z-index cannot change the bounding rect of the shape, so // no united updates needed d->shapes.at(i)->setZIndex(d->previousIndexes.at(i)); d->shapes.at(i)->update(); } } static void prepare(KoShape *s, QMap > &newOrder, KoShapeManager *manager, KoShapeReorderCommand::MoveShapeType move) { KoShapeContainer *parent = s->parent(); QMap >::iterator it(newOrder.find(parent)); if (it == newOrder.end()) { QList children; if (parent != 0) { children = parent->shapes(); } else { // get all toplevel shapes children = manager->topLevelShapes(); } std::sort(children.begin(), children.end(), KoShape::compareShapeZIndex); // the append and prepend are needed so that the raise/lower of all shapes works as expected. children.append(0); children.prepend(0); it = newOrder.insert(parent, children); } QList & shapes(newOrder[parent]); int index = shapes.indexOf(s); if (index != -1) { shapes.removeAt(index); switch (move) { case KoShapeReorderCommand::BringToFront: index = shapes.size(); break; case KoShapeReorderCommand::RaiseShape: if (index < shapes.size()) { ++index; } break; case KoShapeReorderCommand::LowerShape: if (index > 0) { --index; } break; case KoShapeReorderCommand::SendToBack: index = 0; break; } shapes.insert(index,s); } } // static KoShapeReorderCommand *KoShapeReorderCommand::createCommand(const QList &shapes, KoShapeManager *manager, MoveShapeType move, KUndo2Command *parent) { /** * TODO: this method doesn't handle the case when one of the shapes * has maximum or minimum zIndex value (which is 16-bit in our case)! */ QList newIndexes; QList changedShapes; QMap > newOrder; QList sortedShapes(shapes); std::sort(sortedShapes.begin(), sortedShapes.end(), KoShape::compareShapeZIndex); if (move == BringToFront || move == LowerShape) { for (int i = 0; i < sortedShapes.size(); ++i) { prepare(sortedShapes.at(i), newOrder, manager, move); } } else { for (int i = sortedShapes.size() - 1; i >= 0; --i) { prepare(sortedShapes.at(i), newOrder, manager, move); } } QMap >::iterator newIt(newOrder.begin()); for (; newIt!= newOrder.end(); ++newIt) { QList order(newIt.value()); order.removeAll(0); int index = -KoShape::maxZIndex - 1; // set minimum zIndex int pos = 0; for (; pos < order.size(); ++pos) { if (order[pos]->zIndex() > index) { index = order[pos]->zIndex(); } else { break; } } if (pos == order.size()) { //nothing needs to be done continue; } else if (pos <= order.size() / 2) { // new index for the front int startIndex = order[pos]->zIndex() - pos; for (int i = 0; i < pos; ++i) { changedShapes.append(order[i]); newIndexes.append(startIndex++); } } else { //new index for the end for (int i = pos; i < order.size(); ++i) { changedShapes.append(order[i]); newIndexes.append(++index); } } } Q_ASSERT(changedShapes.count() == newIndexes.count()); return changedShapes.isEmpty() ? 0: new KoShapeReorderCommand(changedShapes, newIndexes, parent); } KoShapeReorderCommand *KoShapeReorderCommand::mergeInShape(QList shapes, KoShape *newShape, KUndo2Command *parent) { std::sort(shapes.begin(), shapes.end(), KoShape::compareShapeZIndex); QList reindexedShapes; QList reindexedIndexes; const int originalShapeZIndex = newShape->zIndex(); int newShapeZIndex = originalShapeZIndex; int lastOccupiedShapeZIndex = originalShapeZIndex + 1; Q_FOREACH (KoShape *shape, shapes) { if (shape == newShape) continue; const int zIndex = shape->zIndex(); if (newShapeZIndex == originalShapeZIndex) { if (zIndex == originalShapeZIndex) { newShapeZIndex = originalShapeZIndex + 1; lastOccupiedShapeZIndex = newShapeZIndex; reindexedShapes << newShape; reindexedIndexes << newShapeZIndex; } } else { if (zIndex >= newShapeZIndex && zIndex <= lastOccupiedShapeZIndex) { lastOccupiedShapeZIndex = zIndex + 1; reindexedShapes << shape; reindexedIndexes << lastOccupiedShapeZIndex; } } } return !reindexedShapes.isEmpty() ? new KoShapeReorderCommand(reindexedShapes, reindexedIndexes, parent) : 0; } QList KoShapeReorderCommand::homogenizeZIndexes(QList shapes) { if (shapes.isEmpty()) return shapes; // the shapes are expected to be sorted, we just need to adjust the indexes int lastIndex = shapes.begin()->zIndex; auto it = shapes.begin() + 1; while (it != shapes.end()) { if (it->zIndex <= lastIndex) { it->zIndex = lastIndex + 1; } lastIndex = it->zIndex; ++it; } const int overflowSize = shapes.last().zIndex - int(std::numeric_limits::max()); if (overflowSize > 0) { if (shapes.first().zIndex - overflowSize > int(std::numeric_limits::min())) { for (auto it = shapes.begin(); it != shapes.end(); ++it) { it->zIndex -= overflowSize; } } else { int index = shapes.size() < int(std::numeric_limits::max()) ? 0 : int(std::numeric_limits::max()) - shapes.size(); for (auto it = shapes.begin(); it != shapes.end(); ++it) { it->zIndex = index; index++; } } } return shapes; } QList KoShapeReorderCommand::homogenizeZIndexesLazy(QList shapes) { shapes = homogenizeZIndexes(shapes); // remove shapes that didn't change for (auto it = shapes.begin(); it != shapes.end();) { if (it->zIndex == it->shape->zIndex()) { it = shapes.erase(it); } else { ++it; } } return shapes; } QList KoShapeReorderCommand::mergeDownShapes(QList shapesBelow, QList shapesAbove) { std::sort(shapesBelow.begin(), shapesBelow.end(), KoShape::compareShapeZIndex); std::sort(shapesAbove.begin(), shapesAbove.end(), KoShape::compareShapeZIndex); QList shapes; Q_FOREACH (KoShape *shape, shapesBelow) { shapes.append(IndexedShape(shape)); } Q_FOREACH (KoShape *shape, shapesAbove) { shapes.append(IndexedShape(shape)); } return homogenizeZIndexesLazy(shapes); } QDebug operator<<(QDebug dbg, const KoShapeReorderCommand::IndexedShape &indexedShape) { dbg.nospace() << "IndexedShape (" << indexedShape.shape << ", " << indexedShape.zIndex << ")"; return dbg.space(); } diff --git a/libs/flake/svg/SvgParser.h b/libs/flake/svg/SvgParser.h index c93e6df8ea..1a067df347 100644 --- a/libs/flake/svg/SvgParser.h +++ b/libs/flake/svg/SvgParser.h @@ -1,222 +1,222 @@ /* This file is part of the KDE project * Copyright (C) 2002-2003,2005 Rob Buis * Copyright (C) 2005-2006 Tim Beaulen * Copyright (C) 2005,2007-2009 Jan Hambrecht * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef SVGPARSER_H #define SVGPARSER_H #include #include #include #include #include #include #include "kritaflake_export.h" #include "SvgGradientHelper.h" #include "SvgFilterHelper.h" #include "SvgClipPathHelper.h" #include "SvgLoadingContext.h" #include "SvgStyleParser.h" #include "KoClipMask.h" #include class KoShape; class KoShapeGroup; class KoShapeContainer; class KoDocumentResourceManager; class KoVectorPatternBackground; class KoMarker; class KoPathShape; class KoSvgTextShape; class KRITAFLAKE_EXPORT SvgParser { class DeferredUseStore; public: explicit SvgParser(KoDocumentResourceManager *documentResourceManager); virtual ~SvgParser(); /// Parses a svg fragment, returning the list of top level child shapes QList parseSvg(const KoXmlElement &e, QSizeF * fragmentSize = 0); /// Sets the initial xml base directory (the directory form where the file is read) void setXmlBaseDir(const QString &baseDir); void setResolution(const QRectF boundsInPixels, qreal pixelsPerInch); - /// A special workaround coeff for usign when loading old ODF-embedded SVG files, + /// A special workaround coeff for using when loading old ODF-embedded SVG files, /// which used hard-coded 96 ppi for font size void setForcedFontSizeResolution(qreal value); /// Returns the list of all shapes of the svg document QList shapes() const; /// Takes the collection of symbols contained in the svg document. The parser will /// no longer know about the symbols. QVector takeSymbols(); QString documentTitle() const; QString documentDescription() const; typedef std::function FileFetcherFunc; void setFileFetcher(FileFetcherFunc func); QList> knownMarkers() const; void parseDefsElement(const KoXmlElement &e); KoShape* parseTextElement(const KoXmlElement &e, KoSvgTextShape *mergeIntoShape = 0); protected: /// Parses a group-like element element, saving all its topmost properties KoShape* parseGroup(const KoXmlElement &e, const KoXmlElement &overrideChildrenFrom = KoXmlElement()); // XXX KoShape* parseTextNode(const KoXmlText &e); /// Parses a container element, returning a list of child shapes QList parseContainer(const KoXmlElement &, bool parseTextNodes = false); /// XXX QList parseSingleElement(const KoXmlElement &b, DeferredUseStore* deferredUseStore = 0); /// Parses a use element, returning a list of child shapes KoShape* parseUse(const KoXmlElement &, DeferredUseStore* deferredUseStore); KoShape* resolveUse(const KoXmlElement &e, const QString& key); /// Parses a gradient element SvgGradientHelper *parseGradient(const KoXmlElement &); /// Parses a pattern element QSharedPointer parsePattern(const KoXmlElement &e, const KoShape *__shape); /// Parses a filter element bool parseFilter(const KoXmlElement &, const KoXmlElement &referencedBy = KoXmlElement()); /// Parses a clip path element bool parseClipPath(const KoXmlElement &); bool parseClipMask(const KoXmlElement &e); bool parseMarker(const KoXmlElement &e); bool parseSymbol(const KoXmlElement &e); /// parses a length attribute qreal parseUnit(const QString &, bool horiz = false, bool vert = false, const QRectF &bbox = QRectF()); /// parses a length attribute in x-direction qreal parseUnitX(const QString &unit); /// parses a length attribute in y-direction qreal parseUnitY(const QString &unit); /// parses a length attribute in xy-direction qreal parseUnitXY(const QString &unit); /// parses a angular attribute values, result in radians qreal parseAngular(const QString &unit); KoShape *createObjectDirect(const KoXmlElement &b); /// Creates an object from the given xml element KoShape * createObject(const KoXmlElement &, const SvgStyles &style = SvgStyles()); /// Create path object from the given xml element KoShape * createPath(const KoXmlElement &); /// find gradient with given id in gradient map SvgGradientHelper* findGradient(const QString &id); /// find pattern with given id in pattern map QSharedPointer findPattern(const QString &id, const KoShape *shape); /// find filter with given id in filter map SvgFilterHelper* findFilter(const QString &id, const QString &href = QString()); /// find clip path with given id in clip path map SvgClipPathHelper* findClipPath(const QString &id); /// Adds list of shapes to the given group shape void addToGroup(QList shapes, KoShapeContainer *group); /// creates a shape from the given shape id KoShape * createShape(const QString &shapeID); /// Creates shape from specified svg element KoShape * createShapeFromElement(const KoXmlElement &element, SvgLoadingContext &context); /// Builds the document from the given shapes list void buildDocument(QList shapes); void uploadStyleToContext(const KoXmlElement &e); void applyCurrentStyle(KoShape *shape, const QPointF &shapeToOriginalUserCoordinates); void applyCurrentBasicStyle(KoShape *shape); /// Applies styles to the given shape void applyStyle(KoShape *, const KoXmlElement &, const QPointF &shapeToOriginalUserCoordinates); /// Applies styles to the given shape void applyStyle(KoShape *, const SvgStyles &, const QPointF &shapeToOriginalUserCoordinates); /// Applies the current fill style to the object void applyFillStyle(KoShape * shape); /// Applies the current stroke style to the object void applyStrokeStyle(KoShape * shape); /// Applies the current filter to the object void applyFilter(KoShape * shape); /// Applies the current clip path to the object void applyClipping(KoShape *shape, const QPointF &shapeToOriginalUserCoordinates); void applyMaskClipping(KoShape *shape, const QPointF &shapeToOriginalUserCoordinates); void applyMarkers(KoPathShape *shape); /// Applies id to specified shape void applyId(const QString &id, KoShape *shape); /// Applies viewBox transformation to the current graphical context - /// NOTE: after applying the function currectBoundingBox can become null! + /// NOTE: after applying the function currentBoundingBox can become null! void applyViewBoxTransform(const KoXmlElement &element); private: QSizeF m_documentSize; SvgLoadingContext m_context; QMap m_gradients; QMap m_filters; QMap m_clipPaths; QMap> m_clipMasks; QMap> m_markers; KoDocumentResourceManager *m_documentResourceManager; QList m_shapes; QVector m_symbols; QList m_toplevelShapes; QList m_defsShapes; bool m_isInsideTextSubtree = false; QString m_documentTitle; QString m_documentDescription; }; #endif diff --git a/libs/flake/svg/SvgSavingContext.cpp b/libs/flake/svg/SvgSavingContext.cpp index 7329a3663c..36a798d658 100644 --- a/libs/flake/svg/SvgSavingContext.cpp +++ b/libs/flake/svg/SvgSavingContext.cpp @@ -1,256 +1,256 @@ /* This file is part of the KDE project * Copyright (C) 2011 Jan Hambrecht * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "SvgSavingContext.h" #include "SvgUtil.h" #include #include #include #include #include #include #include #include #include #include #include #include #include class Q_DECL_HIDDEN SvgSavingContext::Private { public: Private(QIODevice *_mainDevice, QIODevice *_styleDevice) : mainDevice(_mainDevice) , styleDevice(_styleDevice) , styleWriter(0) , shapeWriter(0) , saveInlineImages(true) { styleWriter.reset(new KoXmlWriter(&styleBuffer, 1)); styleWriter->startElement("defs"); shapeWriter.reset(new KoXmlWriter(&shapeBuffer, 1)); const qreal scaleToUserSpace = SvgUtil::toUserSpace(1.0); userSpaceMatrix.scale(scaleToUserSpace, scaleToUserSpace); } ~Private() { } QIODevice *mainDevice; QIODevice *styleDevice; QBuffer styleBuffer; QBuffer shapeBuffer; QScopedPointer styleWriter; QScopedPointer shapeWriter; QHash uniqueNames; QHash shapeIds; QTransform userSpaceMatrix; bool saveInlineImages; bool strippedTextMode = false; }; SvgSavingContext::SvgSavingContext(QIODevice &outputDevice, bool saveInlineImages) : d(new Private(&outputDevice, 0)) { d->saveInlineImages = saveInlineImages; } SvgSavingContext::SvgSavingContext(QIODevice &shapesDevice, QIODevice &styleDevice, bool saveInlineImages) : d(new Private(&shapesDevice, &styleDevice)) { d->saveInlineImages = saveInlineImages; } SvgSavingContext::~SvgSavingContext() { d->styleWriter->endElement(); if (d->styleDevice) { d->styleDevice->write(d->styleBuffer.data()); } else { d->mainDevice->write(d->styleBuffer.data()); d->mainDevice->write("\n"); } d->mainDevice->write(d->shapeBuffer.data()); delete d; } KoXmlWriter &SvgSavingContext::styleWriter() { return *d->styleWriter; } KoXmlWriter &SvgSavingContext::shapeWriter() { return *d->shapeWriter; } QString SvgSavingContext::createUID(const QString &base) { QString idBase = base.isEmpty() ? "defitem" : base; int counter = d->uniqueNames.value(idBase); d->uniqueNames.insert(idBase, counter+1); return idBase + QString("%1").arg(counter); } QString SvgSavingContext::getID(const KoShape *obj) { QString id; // do we have already an id for this object ? if (d->shapeIds.contains(obj)) { // use existing id id = d->shapeIds[obj]; } else { // initialize from object name id = obj->name(); // if object name is not empty and was not used already // we can use it as is if (!id.isEmpty() && !d->uniqueNames.contains(id)) { // add to unique names so it does not get reused d->uniqueNames.insert(id, 1); } else { if (id.isEmpty()) { // differentiate a little between shape types if (dynamic_cast(obj)) id = "group"; else if (dynamic_cast(obj)) id = "layer"; else id = "shape"; } - // create a compeletely new id based on object name + // create a completely new id based on object name // or a generic name id = createUID(id); } // record id for this shape d->shapeIds.insert(obj, id); } return id; } QTransform SvgSavingContext::userSpaceTransform() const { return d->userSpaceMatrix; } bool SvgSavingContext::isSavingInlineImages() const { return d->saveInlineImages; } QString SvgSavingContext::createFileName(const QString &extension) { QFile *file = qobject_cast(d->mainDevice); if (!file) return QString(); QFileInfo fi(file->fileName()); QString path = fi.absolutePath(); QString dstBaseFilename = fi.baseName(); // create a filename for the image file at the destination directory QString fname = dstBaseFilename + '_' + createUID("file"); // check if file exists already int i = 0; QString counter; // change filename as long as the filename already exists while (QFile(path + fname + counter + extension).exists()) { counter = QString("_%1").arg(++i); } return fname + counter + extension; } QString SvgSavingContext::saveImage(const QImage &image) { if (isSavingInlineImages()) { QByteArray ba; QBuffer buffer(&ba); buffer.open(QIODevice::WriteOnly); if (image.save(&buffer, "PNG")) { const QString header("data:image/x-png;base64,"); return header + ba.toBase64(); } } else { // write to a temp file first QTemporaryFile imgFile; if (image.save(&imgFile, "PNG")) { QString dstFilename = createFileName(".png"); if (QFile::copy(imgFile.fileName(), dstFilename)) { return dstFilename; } else { QFile f(imgFile.fileName()); f.remove(); } } } return QString(); } QString SvgSavingContext::saveImage(KoImageData *image) { if (isSavingInlineImages()) { QByteArray ba; QBuffer buffer(&ba); buffer.open(QIODevice::WriteOnly); if (image->saveData(buffer)) { const QString header("data:" + KisMimeDatabase::mimeTypeForSuffix(image->suffix()) + ";base64,"); return header + ba.toBase64(); } } else { // write to a temp file first QTemporaryFile imgFile; if (image->saveData(imgFile)) { QString ext = image->suffix(); QString dstFilename = createFileName(ext); // move the temp file to the destination directory if (QFile::copy(imgFile.fileName(), dstFilename)) { return dstFilename; } else { QFile f(imgFile.fileName()); f.remove(); } } } return QString(); } void SvgSavingContext::setStrippedTextMode(bool value) { d->strippedTextMode = value; } bool SvgSavingContext::strippedTextMode() const { return d->strippedTextMode; } diff --git a/libs/flake/svg/SvgSavingContext.h b/libs/flake/svg/SvgSavingContext.h index f76bc3a077..9b5248ab0b 100644 --- a/libs/flake/svg/SvgSavingContext.h +++ b/libs/flake/svg/SvgSavingContext.h @@ -1,84 +1,84 @@ /* This file is part of the KDE project * Copyright (C) 2011 Jan Hambrecht * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef SVGSAVINGCONTEXT_H #define SVGSAVINGCONTEXT_H #include class KoXmlWriter; class KoShape; class KoImageData; class QIODevice; class QString; class QTransform; class QImage; #include "kritaflake_export.h" /// Context for saving svg files class KRITAFLAKE_EXPORT SvgSavingContext { public: /// Creates a new svg saving context on the specified output device explicit SvgSavingContext(QIODevice &outputDevice, bool saveInlineImages = true); explicit SvgSavingContext(QIODevice &shapesDevice, QIODevice &styleDevice, bool saveInlineImages = true); /// Virtual destructor virtual ~SvgSavingContext(); /// Provides access to the style writer KoXmlWriter &styleWriter(); /// Provides access to the shape writer KoXmlWriter &shapeWriter(); - /// Create a unqiue id from the specified base text + /// Create a unique id from the specified base text QString createUID(const QString &base); /// Returns the unique id for the given shape QString getID(const KoShape *obj); /// Returns the transformation used to transform into usre space QTransform userSpaceTransform() const; /// Returns if image should be saved inline bool isSavingInlineImages() const; /// Create a filename suitable for saving external data QString createFileName(const QString &extension); /// Saves given image and returns the href used QString saveImage(const QImage &image); /// Saves given image and returns the href used QString saveImage(KoImageData *image); void setStrippedTextMode(bool value); bool strippedTextMode() const; private: Q_DISABLE_COPY(SvgSavingContext) private: class Private; Private * const d; }; #endif // SVGSAVINGCONTEXT_H diff --git a/libs/flake/tests/TestImageCollection.cpp b/libs/flake/tests/TestImageCollection.cpp index e495ad04d5..5eb4dde7d0 100644 --- a/libs/flake/tests/TestImageCollection.cpp +++ b/libs/flake/tests/TestImageCollection.cpp @@ -1,249 +1,249 @@ /* * This file is part of Calligra tests * * Copyright (C) 2006-2010 Thomas Zander * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "TestImageCollection.h" #include #include #include #include #include #include #include #include #include void TestImageCollection::testGetImageImage() { KoImageCollection collection; QImage image(FILES_DATA_DIR "logo-calligra.png"); KoImageData *id1 = collection.createImageData(image); QCOMPARE(id1->hasCachedImage(), true); QCOMPARE(id1->suffix(), QString("png")); KoImageData *id2 = collection.createImageData(image); QCOMPARE(id2->hasCachedImage(), true); QCOMPARE(id1->priv(), id2->priv()); KoImageData *id3 = collection.createImageData(image); QCOMPARE(id3->hasCachedImage(), true); QCOMPARE(id1->key(), id3->key()); QCOMPARE(id1->priv(), id3->priv()); QImage image2(FILES_DATA_DIR "logo-kpresenter.png"); KoImageData *id4 = collection.createImageData(image2); QCOMPARE(id4->hasCachedImage(), true); QVERIFY(id1->key() != id4->key()); QCOMPARE(collection.count(), 2); delete id4; QCOMPARE(id1->hasCachedImage(), true); QCOMPARE(id2->hasCachedImage(), true); QCOMPARE(id3->hasCachedImage(), true); QCOMPARE(collection.count(), 1); delete id1; QCOMPARE(id2->hasCachedImage(), true); QCOMPARE(id3->hasCachedImage(), true); QCOMPARE(collection.size(), 1); delete id2; QCOMPARE(id3->hasCachedImage(), true); QCOMPARE(collection.size(), 1); delete id3; QCOMPARE(collection.size(), 0); // add an image bigger than the allowed size to be held in memory QImage hugeImage(500, 500, QImage::Format_RGB32); KoImageData *id5 = collection.createImageData(hugeImage); delete id5; } void TestImageCollection::testGetImageStore() { KoImageCollection collection; KoStore *store = KoStore::createStore(FILES_DATA_DIR "store.zip", KoStore::Read); QString image("logo-calligra.jpg"); KoImageData *id1 = collection.createImageData(image, store); QCOMPARE(id1->suffix(), QString("jpg")); QCOMPARE(id1->hasCachedImage(), false); KoImageData *id2 = collection.createImageData(image, store); QCOMPARE(id2->hasCachedImage(), false); QCOMPARE(id1->priv(), id2->priv()); QCOMPARE(id1->key(), id2->key()); QCOMPARE(collection.count(), 1); delete store; } void TestImageCollection::testInvalidImageData() { KoImageCollection collection; QByteArray invalidImageData(100, '^'); KoImageData *data = collection.createImageData(invalidImageData); QVERIFY(data); QVERIFY(!data->isValid()); QVERIFY(data->errorCode() == KoImageData::OpenFailed); QCOMPARE(collection.count(), 1); QBuffer storedData; QVERIFY(!data->saveData(storedData)); // should fail if QIODevice is closed storedData.open(QIODevice::WriteOnly); QVERIFY(data->saveData(storedData)); QCOMPARE(invalidImageData, storedData.buffer()); delete data; } void TestImageCollection::testImageDataAsSharedData() { KoImageData data; QCOMPARE(data.isValid(), false); QImage image(100, 101, QImage::Format_RGB32); data.setImage(image); QCOMPARE(data.isValid(), true); QCOMPARE(data.hasCachedImage(), true); QCOMPARE(data.image(), image); KoImageData data2(data); QCOMPARE(data, data2); QCOMPARE(data.isValid(), true); QCOMPARE(data.image(), image); QCOMPARE(data2.isValid(), true); QCOMPARE(data2.image(), image); { KoImageData data3; data3 = data; QCOMPARE(data3.isValid(), true); QCOMPARE(data3.image(), image); } QCOMPARE(data, data2); QCOMPARE(data.isValid(), true); QCOMPARE(data.image(), image); QCOMPARE(data2.isValid(), true); QCOMPARE(data2.image(), image); KoImageData empty; KoImageData second(empty); } void TestImageCollection::testPreload1() { KoImageData data; QImage image(100, 102, QImage::Format_RGB32); data.setImage(image); QCOMPARE(data.hasCachedImage(), true); QCOMPARE(data.hasCachedPixmap(), false); QPixmap pixmap = data.pixmap(QSize(40, 41)); QCOMPARE(pixmap.width(), 40); QCOMPARE(pixmap.height(), 41); QCOMPARE(data.hasCachedPixmap(), true); QPixmap pixmap2 = data.pixmap(QSize(40, 41)); QCOMPARE(pixmap.cacheKey(), pixmap2.cacheKey()); QPixmap pixmap3 = data.pixmap(); QCOMPARE(pixmap.cacheKey(), pixmap3.cacheKey()); QCOMPARE(data.hasCachedImage(), true); } void TestImageCollection::testPreload3() { KoImageData data; KoStore *store = KoStore::createStore(FILES_DATA_DIR "store.zip", KoStore::Read); QString image("logo-calligra.png"); data.setImage(image, store); QCOMPARE(data.hasCachedImage(), true); // the png is tiny.. Its kept in memory. QCOMPARE(data.hasCachedPixmap(), false); QPixmap pixmap = data.pixmap(QSize(40, 41)); QCOMPARE(pixmap.width(), 40); QCOMPARE(pixmap.height(), 41); QCOMPARE(data.hasCachedPixmap(), true); QCOMPARE(data.hasCachedImage(), true); QPixmap pixmap2 = data.pixmap(QSize(40, 41)); QCOMPARE(pixmap.cacheKey(), pixmap2.cacheKey()); QPixmap pixmap3 = data.pixmap(); QCOMPARE(pixmap.cacheKey(), pixmap3.cacheKey()); - // now get a differen size; + // now get a different size; QPixmap pixmap4 = data.pixmap(QSize(10, 12)); QCOMPARE(pixmap.width(), 40); QCOMPARE(pixmap.height(), 41); QCOMPARE(pixmap4.width(), 10); QCOMPARE(pixmap4.height(), 12); QVERIFY(pixmap.cacheKey() != pixmap4.cacheKey()); QPixmap pixmap5 = data.pixmap(); QCOMPARE(pixmap5.cacheKey(), pixmap4.cacheKey()); } void TestImageCollection::testSameKey() { KoStore *store = KoStore::createStore(FILES_DATA_DIR "store.zip", KoStore::Read); QString image("logo-calligra.png"); KoImageData data; data.setImage(image, store); KoImageData data2; data2.setImage(image, store); QCOMPARE(data.key(), data2.key()); QFile file(FILES_DATA_DIR "logo-calligra.png"); file.open(QIODevice::ReadOnly); QByteArray imageData = file.readAll(); KoImageData data3; data3.setImage(imageData); QCOMPARE(data.key(), data3.key()); QCOMPARE(data2.key(), data3.key()); QImage qImage1(FILES_DATA_DIR "logo-calligra.png"); QImage qImage2(FILES_DATA_DIR "logo-calligra.png"); KoImageData data4; data4.setImage(qImage1); KoImageData data5; data5.setImage(qImage2); QCOMPARE(data4.key(), data5.key()); QImage qImage3(FILES_DATA_DIR "/logo-calligra-big.png"); QImage qImage4(FILES_DATA_DIR "/logo-calligra-big.png"); KoImageData data6; data6.setImage(qImage3); KoImageData data7; data7.setImage(qImage4); QCOMPARE(data6.key(), data7.key()); // should reset the key so it's the same as data6 data2.setImage(qImage3); QCOMPARE(data2.key(), data7.key()); } void TestImageCollection::testIsValid() { KoImageData data; QCOMPARE(data.isValid(), false); QImage image(100, 102, QImage::Format_RGB32); data.setImage(image); QCOMPARE(data.isValid(), true); QByteArray bytes("foo"); data.setImage(bytes); // obviously not a correct image. QCOMPARE(data.isValid(), false); } QTEST_MAIN(TestImageCollection) diff --git a/libs/flake/text/KoSvgTextShape.cpp b/libs/flake/text/KoSvgTextShape.cpp index 9f9fdcaed1..eafa86e8be 100644 --- a/libs/flake/text/KoSvgTextShape.cpp +++ b/libs/flake/text/KoSvgTextShape.cpp @@ -1,629 +1,629 @@ /* * Copyright (c) 2017 Dmitry Kazakov * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "KoSvgTextShape.h" #include #include #include "KoSvgText.h" #include "KoSvgTextProperties.h" #include #include #include #include #include #include "kis_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include struct KoSvgTextShapePrivate : public KoSvgTextChunkShapePrivate { KoSvgTextShapePrivate(KoSvgTextShape *_q) : KoSvgTextChunkShapePrivate(_q) { } KoSvgTextShapePrivate(const KoSvgTextShapePrivate &rhs, KoSvgTextShape *q) : KoSvgTextChunkShapePrivate(rhs, q) { } std::vector> cachedLayouts; std::vector cachedLayoutsOffsets; QThread *cachedLayoutsWorkingThread = 0; void clearAssociatedOutlines(KoShape *rootShape); Q_DECLARE_PUBLIC(KoSvgTextShape) }; KoSvgTextShape::KoSvgTextShape() : KoSvgTextChunkShape(new KoSvgTextShapePrivate(this)) { Q_D(KoSvgTextShape); setShapeId(KoSvgTextShape_SHAPEID); } KoSvgTextShape::KoSvgTextShape(const KoSvgTextShape &rhs) : KoSvgTextChunkShape(new KoSvgTextShapePrivate(*rhs.d_func(), this)) { Q_D(KoSvgTextShape); setShapeId(KoSvgTextShape_SHAPEID); // QTextLayout has no copy-ctor, so just relayout everything! relayout(); } KoSvgTextShape::~KoSvgTextShape() { } KoShape *KoSvgTextShape::cloneShape() const { return new KoSvgTextShape(*this); } void KoSvgTextShape::shapeChanged(ChangeType type, KoShape *shape) { KoSvgTextChunkShape::shapeChanged(type, shape); if (type == StrokeChanged || type == BackgroundChanged || type == ContentChanged) { relayout(); } } void KoSvgTextShape::paintComponent(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { Q_D(KoSvgTextShape); Q_UNUSED(paintContext); /** * HACK ALERT: - * QTextLayout should only be accessed from the tread it has been created in. + * QTextLayout should only be accessed from the thread it has been created in. * If the cached layout has been created in a different thread, we should just * recreate the layouts in the current thread to be able to render them. */ if (QThread::currentThread() != d->cachedLayoutsWorkingThread) { relayout(); } applyConversion(painter, converter); for (int i = 0; i < (int)d->cachedLayouts.size(); i++) { d->cachedLayouts[i]->draw(&painter, d->cachedLayoutsOffsets[i]); } /** * HACK ALERT: * The layouts of non-gui threads must be destroyed in the same thread * they have been created. Because the thread might be restarted in the * meantime or just destroyed, meaning that the per-thread freetype data * will not be available. */ if (QThread::currentThread() != qApp->thread()) { d->cachedLayouts.clear(); d->cachedLayoutsOffsets.clear(); d->cachedLayoutsWorkingThread = 0; } } void KoSvgTextShape::paintStroke(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { Q_UNUSED(painter); Q_UNUSED(converter); Q_UNUSED(paintContext); // do nothing! everything is painted in paintComponent() } QPainterPath KoSvgTextShape::textOutline() { Q_D(KoSvgTextShape); QPainterPath result; result.setFillRule(Qt::WindingFill); for (int i = 0; i < (int)d->cachedLayouts.size(); i++) { const QPointF layoutOffset = d->cachedLayoutsOffsets[i]; const QTextLayout *layout = d->cachedLayouts[i].get(); for (int j = 0; j < layout->lineCount(); j++) { QTextLine line = layout->lineAt(j); Q_FOREACH (const QGlyphRun &run, line.glyphRuns()) { const QVector indexes = run.glyphIndexes(); const QVector positions = run.positions(); const QRawFont font = run.rawFont(); KIS_SAFE_ASSERT_RECOVER(indexes.size() == positions.size()) { continue; } for (int k = 0; k < indexes.size(); k++) { QPainterPath glyph = font.pathForGlyph(indexes[k]); glyph.translate(positions[k] + layoutOffset); result += glyph; } const qreal thickness = font.lineThickness(); const QRectF runBounds = run.boundingRect(); if (run.overline()) { // the offset is calculated to be consistent with the way how Qt renders the text const qreal y = line.y(); QRectF overlineBlob(runBounds.x(), y, runBounds.width(), thickness); overlineBlob.translate(layoutOffset); QPainterPath path; path.addRect(overlineBlob); - // don't use direct addRect, because it does't care about Qt::WindingFill + // don't use direct addRect, because it doesn't care about Qt::WindingFill result += path; } if (run.strikeOut()) { // the offset is calculated to be consistent with the way how Qt renders the text const qreal y = line.y() + 0.5 * line.height(); QRectF strikeThroughBlob(runBounds.x(), y, runBounds.width(), thickness); strikeThroughBlob.translate(layoutOffset); QPainterPath path; path.addRect(strikeThroughBlob); - // don't use direct addRect, because it does't care about Qt::WindingFill + // don't use direct addRect, because it doesn't care about Qt::WindingFill result += path; } if (run.underline()) { const qreal y = line.y() + line.ascent() + font.underlinePosition(); QRectF underlineBlob(runBounds.x(), y, runBounds.width(), thickness); underlineBlob.translate(layoutOffset); QPainterPath path; path.addRect(underlineBlob); - // don't use direct addRect, because it does't care about Qt::WindingFill + // don't use direct addRect, because it doesn't care about Qt::WindingFill result += path; } } } } return result; } void KoSvgTextShape::resetTextShape() { KoSvgTextChunkShape::resetTextShape(); relayout(); } struct TextChunk { QString text; QVector formats; Qt::LayoutDirection direction = Qt::LeftToRight; Qt::Alignment alignment = Qt::AlignLeading; struct SubChunkOffset { QPointF offset; int start = 0; }; QVector offsets; boost::optional xStartPos; boost::optional yStartPos; QPointF applyStartPosOverride(const QPointF &pos) const { QPointF result = pos; if (xStartPos) { result.rx() = *xStartPos; } if (yStartPos) { result.ry() = *yStartPos; } return result; } }; QVector mergeIntoChunks(const QVector &subChunks) { QVector chunks; for (auto it = subChunks.begin(); it != subChunks.end(); ++it) { if (it->transformation.startsNewChunk() || it == subChunks.begin()) { TextChunk newChunk = TextChunk(); newChunk.direction = it->format.layoutDirection(); newChunk.alignment = it->format.calculateAlignment(); newChunk.xStartPos = it->transformation.xPos; newChunk.yStartPos = it->transformation.yPos; chunks.append(newChunk); } TextChunk ¤tChunk = chunks.last(); if (it->transformation.hasRelativeOffset()) { TextChunk::SubChunkOffset o; o.start = currentChunk.text.size(); o.offset = it->transformation.relativeOffset(); KIS_SAFE_ASSERT_RECOVER_NOOP(!o.offset.isNull()); currentChunk.offsets.append(o); } QTextLayout::FormatRange formatRange; formatRange.start = currentChunk.text.size(); formatRange.length = it->text.size(); formatRange.format = it->format; currentChunk.formats.append(formatRange); currentChunk.text += it->text; } return chunks; } /** * Qt's QTextLayout has a weird trait, it doesn't count space characters as * distinct characters in QTextLayout::setNumColumns(), that is, if we want to * position a block of text that starts with a space character in a specific * position, QTextLayout will drop this space and will move the text to the left. * * That is why we have a special wrapper object that ensures that no spaces are * dropped and their horizontal advance parameter is taken into account. */ struct LayoutChunkWrapper { LayoutChunkWrapper(QTextLayout *layout) : m_layout(layout) { } QPointF addTextChunk(int startPos, int length, const QPointF &textChunkStartPos) { QPointF currentTextPos = textChunkStartPos; const int lastPos = startPos + length - 1; KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(startPos == m_addedChars, currentTextPos); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(lastPos < m_layout->text().size(), currentTextPos); QTextLine line; std::swap(line, m_danglingLine); if (!line.isValid()) { line = m_layout->createLine(); } // skip all the space characters that were not included into the Qt's text line const int currentLineStart = line.isValid() ? line.textStart() : startPos + length; while (startPos < currentLineStart && startPos <= lastPos) { currentTextPos.rx() += skipSpaceCharacter(startPos); startPos++; } if (startPos <= lastPos) { const int numChars = lastPos - startPos + 1; line.setNumColumns(numChars); line.setPosition(currentTextPos - QPointF(0, line.ascent())); currentTextPos.rx() += line.horizontalAdvance(); // skip all the space characters that were not included into the Qt's text line for (int i = line.textStart() + line.textLength(); i < lastPos; i++) { currentTextPos.rx() += skipSpaceCharacter(i); } } else { // keep the created but unused line for future use std::swap(line, m_danglingLine); } m_addedChars += length; return currentTextPos; } private: qreal skipSpaceCharacter(int pos) { const QTextCharFormat format = formatForPos(pos, m_layout->formats()); const QChar skippedChar = m_layout->text()[pos]; KIS_SAFE_ASSERT_RECOVER_NOOP(skippedChar.isSpace() || !skippedChar.isPrint()); QFontMetrics metrics(format.font()); return metrics.width(skippedChar); } static QTextCharFormat formatForPos(int pos, const QVector &formats) { Q_FOREACH (const QTextLayout::FormatRange &range, formats) { if (pos >= range.start && pos < range.start + range.length) { return range.format; } } KIS_SAFE_ASSERT_RECOVER_NOOP(0 && "pos should be within the bounds of the layouted text"); return QTextCharFormat(); } private: int m_addedChars = 0; QTextLayout *m_layout; QTextLine m_danglingLine; }; void KoSvgTextShape::relayout() { Q_D(KoSvgTextShape); d->cachedLayouts.clear(); d->cachedLayoutsOffsets.clear(); d->cachedLayoutsWorkingThread = QThread::currentThread(); QPointF currentTextPos; QVector textChunks = mergeIntoChunks(layoutInterface()->collectSubChunks()); Q_FOREACH (const TextChunk &chunk, textChunks) { std::unique_ptr layout(new QTextLayout()); QTextOption option; // WARNING: never activate this option! It breaks the RTL text layout! //option.setFlags(QTextOption::ShowTabsAndSpaces); option.setWrapMode(QTextOption::WrapAnywhere); option.setUseDesignMetrics(true); // TODO: investigate if it is needed? option.setTextDirection(chunk.direction); layout->setText(chunk.text); layout->setTextOption(option); layout->setFormats(chunk.formats); layout->setCacheEnabled(true); layout->beginLayout(); currentTextPos = chunk.applyStartPosOverride(currentTextPos); const QPointF anchorPointPos = currentTextPos; int lastSubChunkStart = 0; QPointF lastSubChunkOffset; LayoutChunkWrapper wrapper(layout.get()); for (int i = 0; i <= chunk.offsets.size(); i++) { const bool isFinalPass = i == chunk.offsets.size(); const int length = !isFinalPass ? chunk.offsets[i].start - lastSubChunkStart : chunk.text.size() - lastSubChunkStart; if (length > 0) { currentTextPos += lastSubChunkOffset; currentTextPos = wrapper.addTextChunk(lastSubChunkStart, length, currentTextPos); } if (!isFinalPass) { lastSubChunkOffset = chunk.offsets[i].offset; lastSubChunkStart = chunk.offsets[i].start; } } layout->endLayout(); QPointF diff; if (chunk.alignment & Qt::AlignTrailing || chunk.alignment & Qt::AlignHCenter) { if (chunk.alignment & Qt::AlignTrailing) { diff = currentTextPos - anchorPointPos; } else if (chunk.alignment & Qt::AlignHCenter) { diff = 0.5 * (currentTextPos - anchorPointPos); } // TODO: fix after t2b text implemented diff.ry() = 0; } d->cachedLayouts.push_back(std::move(layout)); d->cachedLayoutsOffsets.push_back(-diff); } d->clearAssociatedOutlines(this); for (int i = 0; i < int(d->cachedLayouts.size()); i++) { const QTextLayout &layout = *d->cachedLayouts[i]; const QPointF layoutOffset = d->cachedLayoutsOffsets[i]; using namespace KoSvgText; Q_FOREACH (const QTextLayout::FormatRange &range, layout.formats()) { const KoSvgCharChunkFormat &format = static_cast(range.format); AssociatedShapeWrapper wrapper = format.associatedShapeWrapper(); const int rangeStart = range.start; const int safeRangeLength = range.length > 0 ? range.length : layout.text().size() - rangeStart; if (safeRangeLength <= 0) continue; const int rangeEnd = range.start + safeRangeLength - 1; const int firstLineIndex = layout.lineForTextPosition(rangeStart).lineNumber(); const int lastLineIndex = layout.lineForTextPosition(rangeEnd).lineNumber(); for (int i = firstLineIndex; i <= lastLineIndex; i++) { const QTextLine line = layout.lineAt(i); // It might happen that the range contains only one (or two) // symbol that is a whitespace symbol. In such a case we should // just skip this (invalid) line. if (!line.isValid()) continue; const int posStart = qMax(line.textStart(), rangeStart); const int posEnd = qMin(line.textStart() + line.textLength() - 1, rangeEnd); const QList glyphRuns = line.glyphRuns(posStart, posEnd - posStart + 1); Q_FOREACH (const QGlyphRun &run, glyphRuns) { const QPointF firstPosition = run.positions().first(); const quint32 firstGlyphIndex = run.glyphIndexes().first(); const QPointF lastPosition = run.positions().last(); const quint32 lastGlyphIndex = run.glyphIndexes().last(); const QRawFont rawFont = run.rawFont(); const QRectF firstGlyphRect = rawFont.boundingRect(firstGlyphIndex).translated(firstPosition); const QRectF lastGlyphRect = rawFont.boundingRect(lastGlyphIndex).translated(lastPosition); QRectF rect = run.boundingRect(); /** * HACK ALERT: there is a bug in a way how Qt calculates boundingRect() * of the glyph run. It doesn't care about left and right bearings * of the border chars in the run, therefore it becomes cropped. * * Here we just add a half-char width margin to both sides * of the glyph run to make sure the glyphs are fully painted. * * BUG: 389528 * BUG: 392068 */ rect.setLeft(qMin(rect.left(), lastGlyphRect.left()) - 0.5 * firstGlyphRect.width()); rect.setRight(qMax(rect.right(), lastGlyphRect.right()) + 0.5 * lastGlyphRect.width()); wrapper.addCharacterRect(rect.translated(layoutOffset)); } } } } } void KoSvgTextShapePrivate::clearAssociatedOutlines(KoShape *rootShape) { KoSvgTextChunkShape *chunkShape = dynamic_cast(rootShape); KIS_SAFE_ASSERT_RECOVER_RETURN(chunkShape); chunkShape->layoutInterface()->clearAssociatedOutline(); Q_FOREACH (KoShape *child, chunkShape->shapes()) { clearAssociatedOutlines(child); } } bool KoSvgTextShape::isRootTextNode() const { return true; } KoSvgTextShapeFactory::KoSvgTextShapeFactory() : KoShapeFactoryBase(KoSvgTextShape_SHAPEID, i18n("Text")) { setToolTip(i18n("SVG Text Shape")); setIconName(koIconNameCStr("x-shape-text")); setLoadingPriority(5); setXmlElementNames(KoXmlNS::svg, QStringList("text")); KoShapeTemplate t; t.name = i18n("SVG Text"); t.iconName = koIconName("x-shape-text"); t.toolTip = i18n("SVG Text Shape"); addTemplate(t); } KoShape *KoSvgTextShapeFactory::createDefaultShape(KoDocumentResourceManager *documentResources) const { qDebug() << "Create default svg text shape"; KoSvgTextShape *shape = new KoSvgTextShape(); shape->setShapeId(KoSvgTextShape_SHAPEID); KoSvgTextShapeMarkupConverter converter(shape); converter.convertFromSvg("Lorem ipsum dolor sit amet, consectetur adipiscing elit.", "", QRectF(0, 0, 200, 60), documentResources->shapeController()->pixelsPerInch()); qDebug() << converter.errors() << converter.warnings(); return shape; } KoShape *KoSvgTextShapeFactory::createShape(const KoProperties *params, KoDocumentResourceManager *documentResources) const { KoSvgTextShape *shape = new KoSvgTextShape(); shape->setShapeId(KoSvgTextShape_SHAPEID); QString svgText = params->stringProperty("svgText", "Lorem ipsum dolor sit amet, consectetur adipiscing elit."); QString defs = params->stringProperty("defs" , ""); QRectF shapeRect = QRectF(0, 0, 200, 60); QVariant rect = params->property("shapeRect"); if (rect.type()==QVariant::RectF) { shapeRect = rect.toRectF(); } KoSvgTextShapeMarkupConverter converter(shape); converter.convertFromSvg(svgText, defs, shapeRect, documentResources->shapeController()->pixelsPerInch()); shape->setBackground(QSharedPointer(new KoColorBackground(QColor(Qt::black)))); shape->setPosition(shapeRect.topLeft()); return shape; } bool KoSvgTextShapeFactory::supports(const KoXmlElement &/*e*/, KoShapeLoadingContext &/*context*/) const { return false; } diff --git a/libs/flake/tools/KoZoomStrategy.h b/libs/flake/tools/KoZoomStrategy.h index 88630de0e0..c0f5c026f4 100644 --- a/libs/flake/tools/KoZoomStrategy.h +++ b/libs/flake/tools/KoZoomStrategy.h @@ -1,60 +1,60 @@ /* This file is part of the KDE project * Copyright (C) 2006-2007 Thomas Zander * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOZOOMSTRATEGY_H #define KOZOOMSTRATEGY_H #include "KoShapeRubberSelectStrategy.h" class KoCanvasController; class KoZoomTool; /** * //internal * This is a strategy for the KoZoomTool which will be used to do the actual zooming */ class KoZoomStrategy : public KoShapeRubberSelectStrategy { public: /** * constructor * @param tool the parent tool this strategy is for * @param controller the canvas controller that wraps the canvas the tool is acting on. - * @param clicked the location (in documnet points) where the interaction starts. + * @param clicked the location (in document points) where the interaction starts. */ KoZoomStrategy(KoZoomTool *tool, KoCanvasController *controller, const QPointF &clicked); void forceZoomOut(); void forceZoomIn(); /// Execute the zoom void finishInteraction(Qt::KeyboardModifiers modifiers) override; void cancelInteraction() override; protected: SelectionMode currentMode() const override; private: KoCanvasController *m_controller; bool m_forceZoomOut; Q_DECLARE_PRIVATE(KoShapeRubberSelectStrategy) }; #endif diff --git a/libs/image/kis_paint_device.h b/libs/image/kis_paint_device.h index 9a4c833391..8c3bff2029 100644 --- a/libs/image/kis_paint_device.h +++ b/libs/image/kis_paint_device.h @@ -1,880 +1,880 @@ /* * Copyright (c) 2002 patrick julien * Copyright (c) 2006 Boudewijn Rempt * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_PAINT_DEVICE_IMPL_H_ #define KIS_PAINT_DEVICE_IMPL_H_ #include #include #include #include "kis_debug.h" #include #include "kis_types.h" #include "kis_shared.h" #include "kis_default_bounds_base.h" #include class KUndo2Command; class QRect; class QImage; class QPoint; class QString; class QColor; class QIODevice; class KoColor; class KoColorSpace; class KoColorProfile; class KisDataManager; class KisPaintDeviceWriter; class KisKeyframe; class KisRasterKeyframeChannel; class KisPaintDeviceFramesInterface; typedef KisSharedPtr KisDataManagerSP; namespace KritaUtils { enum DeviceCopyMode { CopySnapshot = 0, CopyAllFrames }; } /** * A paint device contains the actual pixel data and offers methods * to read and write pixels. A paint device has an integer x, y position * (it is not positioned on the image with sub-pixel accuracy). * A KisPaintDevice doesn't have any fixed size, the size changes dynamically * when pixels are accessed by an iterator. */ class KRITAIMAGE_EXPORT KisPaintDevice : public QObject , public KisShared { Q_OBJECT public: /** * Create a new paint device with the specified colorspace. * * @param colorSpace the colorspace of this paint device * @param name for debugging purposes */ explicit KisPaintDevice(const KoColorSpace * colorSpace, const QString& name = QString()); /** * Create a new paint device with the specified colorspace. The * parent node will be notified of changes to this paint device. * * @param parent the node that contains this paint device * @param colorSpace the colorspace of this paint device * @param defaultBounds boundaries of the device in case it is empty * @param name for debugging purposes */ KisPaintDevice(KisNodeWSP parent, const KoColorSpace * colorSpace, KisDefaultBoundsBaseSP defaultBounds = KisDefaultBoundsBaseSP(), const QString& name = QString()); /** * Creates a copy of this device. * * If \p copyMode is CopySnapshot, the newly created device clones the * current frame of \p rhs only (default and efficient * behavior). If \p copyFrames is CopyAllFrames, the new device is a deep * copy of the source with all the frames included. */ KisPaintDevice(const KisPaintDevice& rhs, KritaUtils::DeviceCopyMode copyMode = KritaUtils::CopySnapshot, KisNode *newParentNode = 0); ~KisPaintDevice() override; protected: /** * A special constructor for usage in KisPixelSelection. It allows * two paint devices to share a data manager. * * @param explicitDataManager data manager to use inside paint device * @param src source paint device to copy parameters from * @param name for debugging purposes */ KisPaintDevice(KisDataManagerSP explicitDataManager, KisPaintDeviceSP src, const QString& name = QString()); public: /** * Write the pixels of this paint device into the specified file store. */ bool write(KisPaintDeviceWriter &store); /** * Fill this paint device with the pixels from the specified file store. */ bool read(QIODevice *stream); public: /** * set the parent node of the paint device */ void setParentNode(KisNodeWSP parent); /** * set the default bounds for the paint device when * the default pixel is not completely transparent */ void setDefaultBounds(KisDefaultBoundsBaseSP bounds); /** * the default bounds rect of the paint device */ KisDefaultBoundsBaseSP defaultBounds() const; /** * Moves the device to these new coordinates (no incremental move) */ void moveTo(qint32 x, qint32 y); /** * Convenience method for the above. */ virtual void moveTo(const QPoint& pt); /** * Return an X,Y offset of the device in a convenient form */ QPoint offset() const; /** * The X offset of the paint device */ qint32 x() const; /** * The Y offset of the paint device */ qint32 y() const; /** * set the X offset of the paint device */ void setX(qint32 x); /** * set the Y offset of the paint device */ void setY(qint32 y); /** * Retrieve the bounds of the paint device. The size is not exact, * but may be larger if the underlying datamanager works that way. * For instance, the tiled datamanager keeps the extent to the nearest * multiple of 64. * * If default pixel is not transparent, then the actual extent * rect is united with the defaultBounds()->bounds() value * (the size of the image, usually). */ QRect extent() const; /// Convenience method for the above void extent(qint32 &x, qint32 &y, qint32 &w, qint32 &h) const; /** * Get the exact bounds of this paint device. The real solution is * very slow because it does a linear scanline search, but it * uses caching, so calling to this function without changing * the device is quite cheap. * * Exactbounds follows these rules: * *

    *
  • if default pixel is transparent, then exact bounds * of actual pixel data are returned *
  • if default pixel is not transparent, then the union * (defaultBounds()->bounds() | nonDefaultPixelArea()) is * returned *
* \see calculateExactBounds() */ QRect exactBounds() const; /** * Relaxed version of the exactBounds() that can be used in tight * loops. If the exact bounds value is present in the paint * device cache, returns this value. If the cache is invalidated, * returns extent() and tries to recalculate the exact bounds not * faster than once in 1000 ms. */ QRect exactBoundsAmortized() const; /** * Returns exact rectangle of the paint device that contains * non-default pixels. For paint devices with fully transparent * default pixel is equivalent to exactBounds(). * * nonDefaultPixelArea() follows these rules: * *
    *
  • if default pixel is transparent, then exact bounds * of actual pixel data are returned. The same as exactBounds() *
  • if default pixel is not transparent, then calculates the * rectangle of non-default pixels. May be smaller or greater * than image bounds *
* \see calculateExactBounds() */ QRect nonDefaultPixelArea() const; /** * Returns a rough approximation of region covered by device. * For tiled data manager, it region will consist of a number * of rects each corresponding to a tile. */ QRegion region() const; /** * The slow version of region() that searches for exact bounds of * each rectangle in the region */ QRegion regionExact() const; /** * Cut the paint device down to the specified rect. If the crop * area is bigger than the paint device, nothing will happen. */ void crop(qint32 x, qint32 y, qint32 w, qint32 h); /// Convenience method for the above void crop(const QRect & r); /** * Complete erase the current paint device. Its size will become 0. This * does not take the selection into account. */ virtual void clear(); /** * Clear the given rectangle to transparent black. The paint device will expand to * contain the given rect. */ void clear(const QRect & rc); /** * Frees the memory occupied by the pixels containing default * values. The extents() and exactBounds() of the image will * probably also shrink */ void purgeDefaultPixels(); /** * Sets the default pixel. New data will be initialised with this pixel. The pixel is copied: the * caller still owns the pointer and needs to delete it to avoid memory leaks. * If frame ID is given, set default pixel for that frame. Otherwise use active frame. */ void setDefaultPixel(const KoColor &defPixel); /** * Get a pointer to the default pixel. * If the frame parameter is given, get the default pixel of * specified frame. Otherwise use currently active frame. */ KoColor defaultPixel() const; /** * Fill the given rectangle with the given pixel. The paint device will expand to * contain the given rect. */ void fill(const QRect & rc, const KoColor &color); /** * Overloaded function. For legacy purposes only. * Please use fill(const QRect & rc, const KoColor &color) instead */ void fill(qint32 x, qint32 y, qint32 w, qint32 h, const quint8 *fillPixel); public: /** * Prepares the device for fastBitBlt opreration. It clears * the device, switches x,y shifts and colorspace if needed. * After this call fastBitBltPossible will return true. * May be used for initialization of temporary devices. */ void prepareClone(KisPaintDeviceSP src); /** * Make this device to become a clone of \a src. It will have the same * x,y shifts, colorspace and will share pixels inside \a rect. * After calling this function: * (this->extent() >= this->exactBounds() == rect). * * Rule of thumb: * * "Use makeCloneFrom() or makeCloneFromRough() if and only if you * are the only owner of the destination paint device and you are * 100% sure no other thread has access to it" */ void makeCloneFrom(KisPaintDeviceSP src, const QRect &rect); /** * Make this device to become a clone of \a src. It will have the same * x,y shifts, colorspace and will share pixels inside \a rect. * Be careful, this function will copy *at least* \a rect * of pixels. Actual copy area will be a bigger - it will * be aligned by tiles borders. So after calling this function: * (this->extent() == this->exactBounds() >= rect). * * Rule of thumb: * * "Use makeCloneFrom() or makeCloneFromRough() if and only if you * are the only owner of the destination paint device and you are * 100% sure no other thread has access to it" */ void makeCloneFromRough(KisPaintDeviceSP src, const QRect &minimalRect); protected: friend class KisPaintDeviceTest; friend class DataReaderThread; /** * Checks whether a src paint device can be used as source * of fast bitBlt operation. The result of the check may - * depend on whether color spaces coinside, whether there is + * depend on whether color spaces coincide, whether there is * any shift of tiles between the devices and etc. * * WARNING: This check must be done before performing any * fast bitBlt operation! * * \see fastBitBlt * \see fastBitBltRough */ bool fastBitBltPossible(KisPaintDeviceSP src); /** * Clones rect from another paint device. The cloned area will be * shared between both paint devices as much as possible using * copy-on-write. Parts of the rect that cannot be shared * (cross tiles) are deep-copied, * * \see fastBitBltPossible * \see fastBitBltRough */ void fastBitBlt(KisPaintDeviceSP src, const QRect &rect); /** * The same as \ref fastBitBlt() but reads old data */ void fastBitBltOldData(KisPaintDeviceSP src, const QRect &rect); /** * Clones rect from another paint device in a rough and fast way. * All the tiles touched by rect will be shared, between both * devices, that means it will copy a bigger area than was * requested. This method is supposed to be used for bitBlt'ing * into temporary paint devices. * * \see fastBitBltPossible * \see fastBitBlt */ void fastBitBltRough(KisPaintDeviceSP src, const QRect &rect); /** * The same as \ref fastBitBltRough() but reads old data */ void fastBitBltRoughOldData(KisPaintDeviceSP src, const QRect &rect); public: /** * Read the bytes representing the rectangle described by x, y, w, h into * data. If data is not big enough, Krita will gladly overwrite the rest * of your precious memory. * * Since this is a copy, you need to make sure you have enough memory. * * Reading from areas not previously initialized will read the default * pixel value into data but not initialize that region. */ void readBytes(quint8 * data, qint32 x, qint32 y, qint32 w, qint32 h) const; /** * Read the bytes representing the rectangle rect into * data. If data is not big enough, Krita will gladly overwrite the rest * of your precious memory. * * Since this is a copy, you need to make sure you have enough memory. * * Reading from areas not previously initialized will read the default * pixel value into data but not initialize that region. * @param data The address of the memory to receive the bytes read * @param rect The rectangle in the paint device to read from */ void readBytes(quint8 * data, const QRect &rect) const; /** * Copy the bytes in data into the rect specified by x, y, w, h. If the * data is too small or uninitialized, Krita will happily read parts of * memory you never wanted to be read. * * If the data is written to areas of the paint device not previously initialized, * the paint device will grow. */ void writeBytes(const quint8 * data, qint32 x, qint32 y, qint32 w, qint32 h); /** * Copy the bytes in data into the rectangle rect. If the * data is too small or uninitialized, Krita will happily read parts of * memory you never wanted to be read. * * If the data is written to areas of the paint device not previously initialized, * the paint device will grow. * @param data The address of the memory to write bytes from * @param rect The rectangle in the paint device to write to */ void writeBytes(const quint8 * data, const QRect &rect); /** * Copy the bytes in the paint device into a vector of arrays of bytes, * where the number of arrays is the number of channels in the * paint device. If the specified area is larger than the paint * device's extent, the default pixel will be read. */ QVector readPlanarBytes(qint32 x, qint32 y, qint32 w, qint32 h) const; /** * Write the data in the separate arrays to the channes. If there * are less vectors than channels, the remaining channels will not * be copied. If any of the arrays points to 0, the channel in * that location will not be touched. If the specified area is * larger than the paint device, the paint device will be * extended. There are no guards: if the area covers more pixels * than there are bytes in the arrays, krita will happily fill * your paint device with areas of memory you never wanted to be * read. Krita may also crash. * * XXX: what about undo? */ void writePlanarBytes(QVector planes, qint32 x, qint32 y, qint32 w, qint32 h); /** * Converts the paint device to a different colorspace * * @return a command that can be used to undo the conversion. */ KUndo2Command* convertTo(const KoColorSpace * dstColorSpace, KoColorConversionTransformation::Intent renderingIntent = KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::ConversionFlags conversionFlags = KoColorConversionTransformation::internalConversionFlags()); /** * Changes the profile of the colorspace of this paint device to the given * profile. If the given profile is 0, nothing happens. */ bool setProfile(const KoColorProfile * profile); /** * Fill this paint device with the data from image; starting at (offsetX, offsetY) * @param srcProfileName name of the RGB profile to interpret the image as. 0 is interpreted as sRGB */ void convertFromQImage(const QImage& image, const KoColorProfile *profile, qint32 offsetX = 0, qint32 offsetY = 0); /** * Create an RGBA QImage from a rectangle in the paint device. * * @param x Left coordinate of the rectangle * @param y Top coordinate of the rectangle * @param w Width of the rectangle in pixels * @param h Height of the rectangle in pixels * @param dstProfile RGB profile to use in conversion. May be 0, in which * case it's up to the color strategy to choose a profile (most * like sRGB). */ QImage convertToQImage(const KoColorProfile *dstProfile, qint32 x, qint32 y, qint32 w, qint32 h, KoColorConversionTransformation::Intent renderingIntent = KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::ConversionFlags conversionFlags = KoColorConversionTransformation::internalConversionFlags()) const; /** * Overridden method for convenience */ QImage convertToQImage(const KoColorProfile *dstProfile, const QRect &rc, KoColorConversionTransformation::Intent renderingIntent = KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::ConversionFlags conversionFlags = KoColorConversionTransformation::internalConversionFlags()) const; /** * Create an RGBA QImage from a rectangle in the paint device. The * rectangle is defined by the parent image's bounds. * * @param dstProfile RGB profile to use in conversion. May be 0, in which * case it's up to the color strategy to choose a profile (most * like sRGB). */ QImage convertToQImage(const KoColorProfile * dstProfile, KoColorConversionTransformation::Intent renderingIntent = KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::ConversionFlags conversionFlags = KoColorConversionTransformation::internalConversionFlags()) const; /** * Creates a paint device thumbnail of the paint device, retaining * the aspect ratio. The width and height of the returned device * won't exceed \p maxw and \p maxw, but they may be smaller. * * @param maxw: maximum width * @param maxh: maximum height * @param rect: only this rect will be used for the thumbnail * */ KisPaintDeviceSP createThumbnailDevice(qint32 w, qint32 h, QRect rect = QRect(), QRect outputRect = QRect()) const; KisPaintDeviceSP createThumbnailDeviceOversampled(qint32 w, qint32 h, qreal oversample, QRect rect = QRect(), QRect outputRect = QRect()) const; /** * Creates a thumbnail of the paint device, retaining the aspect ratio. * The width and height of the returned QImage won't exceed \p maxw and \p maxw, but they may be smaller. * The colors are not corrected for display! * * @param maxw: maximum width * @param maxh: maximum height * @param rect: only this rect will be used for the thumbnail * @param oversample: ratio used for antialiasing */ QImage createThumbnail(qint32 maxw, qint32 maxh, QRect rect, qreal oversample = 1, KoColorConversionTransformation::Intent renderingIntent = KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::ConversionFlags conversionFlags = KoColorConversionTransformation::internalConversionFlags()); /** * Cached version of createThumbnail(qint32 maxw, qint32 maxh, const KisSelection *selection, QRect rect) */ QImage createThumbnail(qint32 maxw, qint32 maxh, qreal oversample = 1, KoColorConversionTransformation::Intent renderingIntent = KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::ConversionFlags conversionFlags = KoColorConversionTransformation::internalConversionFlags()); /** * Fill c and opacity with the values found at x and y. * * The color values will be transformed from the profile of * this paint device to the display profile. * * @return true if the operation was successful. */ bool pixel(qint32 x, qint32 y, QColor *c) const; /** * Fill kc with the values found at x and y. This method differs * from the above in using KoColor, which can be of any colorspace * * The color values will be transformed from the profile of * this paint device to the display profile. * * @return true if the operation was successful. */ bool pixel(qint32 x, qint32 y, KoColor * kc) const; /** * Set the specified pixel to the specified color. Note that this * bypasses KisPainter. the PaintDevice is here used as an equivalent * to QImage, not QPixmap. This means that this is not undoable; also, * there is no compositing with an existing value at this location. * * The color values will be transformed from the display profile to * the paint device profile. * * Note that this will use 8-bit values and may cause a significant * degradation when used on 16-bit or hdr quality images. * * @return true if the operation was successful */ bool setPixel(qint32 x, qint32 y, const QColor& c); /// Convenience method for the above bool setPixel(qint32 x, qint32 y, const KoColor& kc); /** * @return the colorspace of the pixels in this paint device */ const KoColorSpace* colorSpace() const; /** * There is quite a common technique in Krita. It is used in * cases, when we want to paint something over a paint device * using the composition, opacity or selection. E.g. painting a * dab in a paint op, filling the selection in the Fill Tool. * Such work is usually done in the following way: * * 1) Create a paint device * * 2) Fill it with the desired color or data * * 3) Create a KisPainter and set all the properties of the * transaction: selection, compositeOp, opacity and etc. * * 4) Paint a newly created paint device over the destination * device. * * The following two methods (createCompositionSourceDevice() or * createCompositionSourceDeviceFixed())should be used for the * accomplishing the step 1). The point is that the desired color * space of the temporary device may not coincide with the color * space of the destination. That is the case, for example, for * the alpha8() colorspace used in the selections. So for such * devices the temporary target would have a different (grayscale) * color space. * * So there are two rules of thumb: * * 1) If you need a temporary device which you are going to fill * with some data and then paint over the paint device, create * it with either createCompositionSourceDevice() or * createCompositionSourceDeviceFixed(). * * 2) Do *not* expect that the color spaces of the destination and * the temporary device would coincide. If you need to copy a * single pixel from one device to another, you can use * KisCrossDeviceColorPicker class, that will handle all the * necessary conversions for you. * * \see createCompositionSourceDeviceFixed() * \see compositionSourceColorSpace() * \see KisCrossDeviceColorPicker * \see KisCrossDeviceColorPickerInt */ KisPaintDeviceSP createCompositionSourceDevice() const; /** * The same as createCompositionSourceDevice(), but initializes the * newly created device with the content of \p cloneSource * * \see createCompositionSourceDevice() */ KisPaintDeviceSP createCompositionSourceDevice(KisPaintDeviceSP cloneSource) const; /** * The same as createCompositionSourceDevice(), but initializes * the newly created device with the *rough* \p roughRect of * \p cloneSource. * * "Rough rect" means that it may copy a bit more than * requested. It is expected that the caller will not use the area * outside \p roughRect. * * \see createCompositionSourceDevice() */ KisPaintDeviceSP createCompositionSourceDevice(KisPaintDeviceSP cloneSource, const QRect roughRect) const; /** * This is a convenience method for createCompositionSourceDevice() * * \see createCompositionSourceDevice() */ KisFixedPaintDeviceSP createCompositionSourceDeviceFixed() const; /** * This is a lowlevel method for the principle used in * createCompositionSourceDevice(). In most of the cases the paint * device creation methods should be used instead of this function. * * \see createCompositionSourceDevice() * \see createCompositionSourceDeviceFixed() */ virtual const KoColorSpace* compositionSourceColorSpace() const; /** * @return the internal datamanager that keeps the pixels. */ KisDataManagerSP dataManager() const; /** * Replace the pixel data, color strategy, and profile. */ void setDataManager(KisDataManagerSP data, const KoColorSpace * colorSpace = 0); /** * Return the number of bytes a pixel takes. */ quint32 pixelSize() const; /** * Return the number of channels a pixel takes */ quint32 channelCount() const; /** * Create a keyframe channel for the content on this device. * @param id identifier for the channel * @param node the parent node for the channel * @return keyframe channel */ KisRasterKeyframeChannel *createKeyframeChannel(const KoID &id); KisRasterKeyframeChannel* keyframeChannel() const; /** * An interface to modify/load/save frames stored inside this device */ KisPaintDeviceFramesInterface* framesInterface(); public: /** * Add the specified rect to the parent layer's set of dirty rects * (if there is a parent layer) */ void setDirty(const QRect & rc); /** * Add the specified region to the parent layer's dirty region * (if there is a parent layer) */ void setDirty(const QRegion & region); /** * Set the parent layer completely dirty, if this paint device has * as parent layer. */ void setDirty(); void setDirty(const QVector rects); /** * Called by KisTransactionData when it thinks current time should * be changed. And the requests is forwarded to the image if * needed. */ void requestTimeSwitch(int time); /** * \return a sequence number corresponding to the current paint * device state. Every time the paint device is changed, * the sequence number is increased */ int sequenceNumber() const; void estimateMemoryStats(qint64 &imageData, qint64 &temporaryData, qint64 &lodData) const; public: KisHLineIteratorSP createHLineIteratorNG(qint32 x, qint32 y, qint32 w); KisHLineConstIteratorSP createHLineConstIteratorNG(qint32 x, qint32 y, qint32 w) const; KisVLineIteratorSP createVLineIteratorNG(qint32 x, qint32 y, qint32 h); KisVLineConstIteratorSP createVLineConstIteratorNG(qint32 x, qint32 y, qint32 h) const; KisRandomAccessorSP createRandomAccessorNG(qint32 x, qint32 y); KisRandomConstAccessorSP createRandomConstAccessorNG(qint32 x, qint32 y) const; /** * Create an iterator that will "artificially" extend the paint device with the * value of the border when trying to access values outside the range of data. * * @param rc indicates the rectangle that truly contains data */ KisRepeatHLineConstIteratorSP createRepeatHLineConstIterator(qint32 x, qint32 y, qint32 w, const QRect& _dataWidth) const; /** * Create an iterator that will "artificially" extend the paint device with the * value of the border when trying to access values outside the range of data. * - * @param rc indicates the rectangle that trully contains data + * @param rc indicates the rectangle that truly contains data */ KisRepeatVLineConstIteratorSP createRepeatVLineConstIterator(qint32 x, qint32 y, qint32 h, const QRect& _dataWidth) const; /** * This function create a random accessor which can easily access to sub pixel values. * @param selection an up-to-date selection that has the same origin as the paint device */ KisRandomSubAccessorSP createRandomSubAccessor() const; /** Clear the selected pixels from the paint device */ void clearSelection(KisSelectionSP selection); Q_SIGNALS: void profileChanged(const KoColorProfile * profile); void colorSpaceChanged(const KoColorSpace *colorspace); public: friend class PaintDeviceCache; /** * Caclculates exact bounds of the device. Used internally * by a transparent caching system. The solution is very slow * because it does a linear scanline search. So the complexity * is n*n at worst. * * \see exactBounds(), nonDefaultPixelArea() */ QRect calculateExactBounds(bool nonDefaultOnly) const; public: struct MemoryReleaseObject : public QObject { ~MemoryReleaseObject() override; }; static MemoryReleaseObject* createMemoryReleaseObject(); public: struct LodDataStruct { virtual ~LodDataStruct(); }; QRegion regionForLodSyncing() const; LodDataStruct* createLodDataStruct(int lod); void updateLodDataStruct(LodDataStruct *dst, const QRect &srcRect); void uploadLodDataStruct(LodDataStruct *dst); void generateLodCloneDevice(KisPaintDeviceSP dst, const QRect &originalRect, int lod); void setProjectionDevice(bool value); void tesingFetchLodDevice(KisPaintDeviceSP targetDevice); private: KisPaintDevice& operator=(const KisPaintDevice&); void init(const KoColorSpace *colorSpace, KisDefaultBoundsBaseSP defaultBounds, KisNodeWSP parent, const QString& name); // Only KisPainter is allowed to have access to these low-level methods friend class KisPainter; /** * Return a vector with in order the size in bytes of the channels * in the colorspace of this paint device. */ QVector channelSizes() const; void emitColorSpaceChanged(); void emitProfileChanged(); private: friend class KisPaintDeviceFramesInterface; protected: friend class KisSelectionTest; KisNodeWSP parentNode() const; private: struct Private; Private * const m_d; }; #endif // KIS_PAINT_DEVICE_IMPL_H_ diff --git a/libs/image/kis_repeat_iterators_pixel.h b/libs/image/kis_repeat_iterators_pixel.h index 7d76bf318a..1c0d18bcc3 100644 --- a/libs/image/kis_repeat_iterators_pixel.h +++ b/libs/image/kis_repeat_iterators_pixel.h @@ -1,268 +1,268 @@ /* * Copyright (c) 2008 Cyrille Berger * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef _KIS_REPEAT_ITERATORS_PIXEL_H_ #define _KIS_REPEAT_ITERATORS_PIXEL_H_ #include #include "kis_shared.h" #include "tiles3/kis_hline_iterator.h" #include "tiles3/kis_vline_iterator.h" template class KisRepeatHLineIteratorPixelBase; template class KisRepeatVLineIteratorPixelBase; /** * This iterator is an iterator that will "artificially" extend the paint device with the * value of the border when trying to access values outside the range of data. */ template class KisRepeatLineIteratorPixelBase : public KisShared { Q_DISABLE_COPY(KisRepeatLineIteratorPixelBase) public: friend class KisRepeatHLineIteratorPixelBase; friend class KisRepeatVLineIteratorPixelBase; /** * @param rc indicates the rectangle that truly contains data */ inline KisRepeatLineIteratorPixelBase(KisDataManager *dm, qint32 x, qint32 y, qint32 offsetx, qint32 offsety, const QRect& _rc, KisIteratorCompleteListener *completeListener); virtual inline ~KisRepeatLineIteratorPixelBase(); public: inline qint32 x() const { return m_realX; } inline qint32 y() const { return m_realY; } inline const quint8 * oldRawData() const { return m_iterator->oldRawData(); } private: KisDataManager* m_dm; qint32 m_realX, m_realY; qint32 m_offsetX, m_offsetY; QRect m_dataRect; T* m_iterator; KisIteratorCompleteListener *m_completeListener; }; /** * This iterator is an iterator that will "artificially" extend the paint device with the * value of the border when trying to access values outside the range of data. */ template class KisRepeatHLineIteratorPixelBase : public KisRepeatLineIteratorPixelBase { public: /** - * @param rc indicates the rectangle that trully contains data + * @param rc indicates the rectangle that truly contains data */ inline KisRepeatHLineIteratorPixelBase(KisDataManager *dm, qint32 x, qint32 y, qint32 w, qint32 offsetx, qint32 offsety, const QRect& _rc, KisIteratorCompleteListener *completeListener); inline ~KisRepeatHLineIteratorPixelBase() override; inline bool nextPixel(); /** * Reach next row. */ inline void nextRow(); private: void createIterator(); private: qint32 m_startX; qint32 m_startIteratorX; qint32 m_width; }; /** * This iterator is an iterator that will "artificially" extend the paint device with the * value of the border when trying to access values outside the range of data. */ template class KisRepeatVLineIteratorPixelBase : public KisRepeatLineIteratorPixelBase { public: /** - * @param rc indicates the rectangle that trully contains data + * @param rc indicates the rectangle that truly contains data */ inline KisRepeatVLineIteratorPixelBase(KisDataManager *dm, qint32 x, qint32 y, qint32 h, qint32 offsetx, qint32 offsety, const QRect& _rc, KisIteratorCompleteListener *completeListener); inline ~KisRepeatVLineIteratorPixelBase() override; inline KisRepeatVLineIteratorPixelBase & operator ++(); inline bool nextPixel(); /** * Reach next row. */ inline void nextColumn(); private: void createIterator(); private: qint32 m_startY; qint32 m_startIteratorY; qint32 m_height; }; //------------------------ Implementations ------------------------// //---------------- KisRepeatLineIteratorPixelBase -----------------// template KisRepeatLineIteratorPixelBase::KisRepeatLineIteratorPixelBase(KisDataManager *dm, qint32 x, qint32 y, qint32 offsetx, qint32 offsety, const QRect& _rc, KisIteratorCompleteListener *completeListener) : m_dm(dm), m_realX(x), m_realY(y), m_offsetX(offsetx), m_offsetY(offsety), m_dataRect(_rc), m_iterator(0), m_completeListener(completeListener) { } template KisRepeatLineIteratorPixelBase::~KisRepeatLineIteratorPixelBase() { delete m_iterator; } //---------------- KisRepeatHLineIteratorPixelBase ----------------// template KisRepeatHLineIteratorPixelBase::KisRepeatHLineIteratorPixelBase(KisDataManager *dm, qint32 x, qint32 y, qint32 w, qint32 offsetx, qint32 offsety, const QRect& _rc, KisIteratorCompleteListener *completeListener) : KisRepeatLineIteratorPixelBase(dm, x, y, offsetx, offsety , _rc, completeListener), m_startX(x), m_startIteratorX(x), m_width(w) { // Compute the startx value of the iterator if (m_startIteratorX < _rc.left()) { m_startIteratorX = _rc.left(); } createIterator(); } template KisRepeatHLineIteratorPixelBase::~KisRepeatHLineIteratorPixelBase() { } template inline bool KisRepeatHLineIteratorPixelBase::nextPixel() { Q_ASSERT(this->m_iterator); if (this->m_realX >= this->m_dataRect.x() && this->m_realX < this->m_dataRect.x() + this->m_dataRect.width() - 1) { this->m_iterator->nextPixel(); } ++this->m_realX; return (this->m_realX < m_startX + m_width); } template inline void KisRepeatHLineIteratorPixelBase::nextRow() { if (this->m_realY >= this->m_dataRect.y() && this->m_realY < this->m_dataRect.y() + this->m_dataRect.height() - 1) { this->m_iterator->nextRow(); } else { createIterator(); } this->m_realX = this->m_startX; ++this->m_realY; } template void KisRepeatHLineIteratorPixelBase::createIterator() { // Cleanup delete this->m_iterator; qint32 startY = this->m_realY; if (startY < this->m_dataRect.y()) { startY = this->m_dataRect.top(); } if (startY > (this->m_dataRect.y() + this->m_dataRect.height() - 1)) { startY = (this->m_dataRect.y() + this->m_dataRect.height() - 1); } int width = this->m_dataRect.x() + this->m_dataRect.width() - this->m_startIteratorX; this->m_iterator = new T(this->m_dm, this->m_startIteratorX, startY, width, this->m_offsetX, this->m_offsetY, false, this->m_completeListener); this->m_realX = this->m_startX; } //---------------- KisRepeatVLineIteratorPixelBase ----------------// template KisRepeatVLineIteratorPixelBase::KisRepeatVLineIteratorPixelBase(KisDataManager *dm, qint32 x, qint32 y, qint32 h, qint32 offsetx, qint32 offsety, const QRect& _rc, KisIteratorCompleteListener *completeListener) : KisRepeatLineIteratorPixelBase(dm, x, y, offsetx, offsety , _rc, completeListener), m_startY(y), m_startIteratorY(y), m_height(h) { // Compute the startx value of the iterator if (m_startIteratorY < _rc.top()) { m_startIteratorY = _rc.top(); } createIterator(); } template KisRepeatVLineIteratorPixelBase::~KisRepeatVLineIteratorPixelBase() { } template inline bool KisRepeatVLineIteratorPixelBase::nextPixel() { Q_ASSERT(this->m_iterator); if (this->m_realY >= this->m_dataRect.y() && this->m_realY < this->m_dataRect.y() + this->m_dataRect.height() - 1) { this->m_iterator->nextPixel(); } ++this->m_realY; return (this->m_realY < m_startY + m_height); } template inline void KisRepeatVLineIteratorPixelBase::nextColumn() { if (this->m_realX >= this->m_dataRect.x() && this->m_realX < this->m_dataRect.x() + this->m_dataRect.width() - 1) { this->m_iterator->nextColumn(); } else { createIterator(); } this->m_realY = this->m_startY; ++this->m_realX; } template void KisRepeatVLineIteratorPixelBase::createIterator() { // Cleanup delete this->m_iterator; qint32 startX = this->m_realX; if (startX < this->m_dataRect.x()) { startX = this->m_dataRect.x(); } if (startX > (this->m_dataRect.x() + this->m_dataRect.width() - 1)) { startX = (this->m_dataRect.x() + this->m_dataRect.width() - 1); } int height = this->m_dataRect.y() + this->m_dataRect.height() - this->m_startIteratorY; this->m_iterator = new T(this->m_dm, startX, this->m_startIteratorY, height, this->m_offsetX, this->m_offsetY, false, this->m_completeListener); this->m_realY = this->m_startY; } #endif diff --git a/libs/image/kis_selection_filters.cpp b/libs/image/kis_selection_filters.cpp index b8a1588356..bcc9dc5d31 100644 --- a/libs/image/kis_selection_filters.cpp +++ b/libs/image/kis_selection_filters.cpp @@ -1,855 +1,855 @@ /* * Copyright (c) 2005 Michael Thaler * Copyright (c) 2011 Dmitry Kazakov * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_selection_filters.h" #include #include #include "kis_convolution_painter.h" #include "kis_convolution_kernel.h" #include "kis_pixel_selection.h" #define MAX(a, b) ((a) > (b) ? (a) : (b)) #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define RINT(x) floor ((x) + 0.5) KisSelectionFilter::~KisSelectionFilter() { } KUndo2MagicString KisSelectionFilter::name() { return KUndo2MagicString(); } QRect KisSelectionFilter::changeRect(const QRect& rect) { return rect; } void KisSelectionFilter::computeBorder(qint32* circ, qint32 xradius, qint32 yradius) { qint32 i; qint32 diameter = xradius * 2 + 1; double tmp; for (i = 0; i < diameter; i++) { if (i > xradius) tmp = (i - xradius) - 0.5; else if (i < xradius) tmp = (xradius - i) - 0.5; else tmp = 0.0; circ[i] = (qint32) RINT(yradius / (double) xradius * sqrt(xradius * xradius - tmp * tmp)); } } void KisSelectionFilter::rotatePointers(quint8** p, quint32 n) { quint32 i; quint8 *p0 = p[0]; for (i = 0; i < n - 1; i++) { p[i] = p[i + 1]; } p[i] = p0; } void KisSelectionFilter::computeTransition(quint8* transition, quint8** buf, qint32 width) { qint32 x = 0; if (width == 1) { if (buf[1][x] > 127 && (buf[0][x] < 128 || buf[2][x] < 128)) transition[x] = 255; else transition[x] = 0; return; } if (buf[1][x] > 127) { if (buf[0][x] < 128 || buf[0][x + 1] < 128 || buf[1][x + 1] < 128 || buf[2][x] < 128 || buf[2][x + 1] < 128) transition[x] = 255; else transition[x] = 0; } else transition[x] = 0; for (qint32 x = 1; x < width - 1; x++) { if (buf[1][x] >= 128) { if (buf[0][x - 1] < 128 || buf[0][x] < 128 || buf[0][x + 1] < 128 || buf[1][x - 1] < 128 || buf[1][x + 1] < 128 || buf[2][x - 1] < 128 || buf[2][x] < 128 || buf[2][x + 1] < 128) transition[x] = 255; else transition[x] = 0; } else transition[x] = 0; } if (buf[1][x] >= 128) { if (buf[0][x - 1] < 128 || buf[0][x] < 128 || buf[1][x - 1] < 128 || buf[2][x - 1] < 128 || buf[2][x] < 128) transition[x] = 255; else transition[x] = 0; } else transition[x] = 0; } KUndo2MagicString KisErodeSelectionFilter::name() { return kundo2_i18n("Erode Selection"); } QRect KisErodeSelectionFilter::changeRect(const QRect& rect) { const qint32 radius = 1; return rect.adjusted(-radius, -radius, radius, radius); } void KisErodeSelectionFilter::process(KisPixelSelectionSP pixelSelection, const QRect& rect) { // Erode (radius 1 pixel) a mask (1bpp) quint8* buf[3]; qint32 width = rect.width(); qint32 height = rect.height(); quint8* out = new quint8[width]; for (qint32 i = 0; i < 3; i++) buf[i] = new quint8[width + 2]; // load top of image pixelSelection->readBytes(buf[0] + 1, rect.x(), rect.y(), width, 1); buf[0][0] = buf[0][1]; buf[0][width + 1] = buf[0][width]; memcpy(buf[1], buf[0], width + 2); for (qint32 y = 0; y < height; y++) { if (y + 1 < height) { pixelSelection->readBytes(buf[2] + 1, rect.x(), rect.y() + y + 1, width, 1); buf[2][0] = buf[2][1]; buf[2][width + 1] = buf[2][width]; } else { memcpy(buf[2], buf[1], width + 2); } for (qint32 x = 0 ; x < width; x++) { qint32 min = 255; if (buf[0][x+1] < min) min = buf[0][x+1]; if (buf[1][x] < min) min = buf[1][x]; if (buf[1][x+1] < min) min = buf[1][x+1]; if (buf[1][x+2] < min) min = buf[1][x+2]; if (buf[2][x+1] < min) min = buf[2][x+1]; out[x] = min; } pixelSelection->writeBytes(out, rect.x(), rect.y() + y, width, 1); rotatePointers(buf, 3); } for (qint32 i = 0; i < 3; i++) delete[] buf[i]; delete[] out; } KUndo2MagicString KisDilateSelectionFilter::name() { return kundo2_i18n("Dilate Selection"); } QRect KisDilateSelectionFilter::changeRect(const QRect& rect) { const qint32 radius = 1; return rect.adjusted(-radius, -radius, radius, radius); } void KisDilateSelectionFilter::process(KisPixelSelectionSP pixelSelection, const QRect& rect) { // dilate (radius 1 pixel) a mask (1bpp) quint8* buf[3]; qint32 width = rect.width(); qint32 height = rect.height(); quint8* out = new quint8[width]; for (qint32 i = 0; i < 3; i++) buf[i] = new quint8[width + 2]; // load top of image pixelSelection->readBytes(buf[0] + 1, rect.x(), rect.y(), width, 1); buf[0][0] = buf[0][1]; buf[0][width + 1] = buf[0][width]; memcpy(buf[1], buf[0], width + 2); for (qint32 y = 0; y < height; y++) { if (y + 1 < height) { pixelSelection->readBytes(buf[2] + 1, rect.x(), rect.y() + y + 1, width, 1); buf[2][0] = buf[2][1]; buf[2][width + 1] = buf[2][width]; } else { memcpy(buf[2], buf[1], width + 2); } for (qint32 x = 0 ; x < width; x++) { qint32 max = 0; if (buf[0][x+1] > max) max = buf[0][x+1]; if (buf[1][x] > max) max = buf[1][x]; if (buf[1][x+1] > max) max = buf[1][x+1]; if (buf[1][x+2] > max) max = buf[1][x+2]; if (buf[2][x+1] > max) max = buf[2][x+1]; out[x] = max; } pixelSelection->writeBytes(out, rect.x(), rect.y() + y, width, 1); rotatePointers(buf, 3); } for (qint32 i = 0; i < 3; i++) delete[] buf[i]; delete[] out; } KisBorderSelectionFilter::KisBorderSelectionFilter(qint32 xRadius, qint32 yRadius) : m_xRadius(xRadius), m_yRadius(yRadius) { } KUndo2MagicString KisBorderSelectionFilter::name() { return kundo2_i18n("Border Selection"); } QRect KisBorderSelectionFilter::changeRect(const QRect& rect) { return rect.adjusted(-m_xRadius, -m_yRadius, m_xRadius, m_yRadius); } void KisBorderSelectionFilter::process(KisPixelSelectionSP pixelSelection, const QRect& rect) { if (m_xRadius <= 0 || m_yRadius <= 0) return; quint8 *buf[3]; quint8 **density; quint8 **transition; if (m_xRadius == 1 && m_yRadius == 1) { // optimize this case specifically quint8* source[3]; for (qint32 i = 0; i < 3; i++) source[i] = new quint8[rect.width()]; quint8* transition = new quint8[rect.width()]; pixelSelection->readBytes(source[0], rect.x(), rect.y(), rect.width(), 1); memcpy(source[1], source[0], rect.width()); if (rect.height() > 1) pixelSelection->readBytes(source[2], rect.x(), rect.y() + 1, rect.width(), 1); else memcpy(source[2], source[1], rect.width()); computeTransition(transition, source, rect.width()); pixelSelection->writeBytes(transition, rect.x(), rect.y(), rect.width(), 1); for (qint32 y = 1; y < rect.height(); y++) { rotatePointers(source, 3); if (y + 1 < rect.height()) pixelSelection->readBytes(source[2], rect.x(), rect.y() + y + 1, rect.width(), 1); else memcpy(source[2], source[1], rect.width()); computeTransition(transition, source, rect.width()); pixelSelection->writeBytes(transition, rect.x(), rect.y() + y, rect.width(), 1); } for (qint32 i = 0; i < 3; i++) delete[] source[i]; delete[] transition; return; } qint32* max = new qint32[rect.width() + 2 * m_xRadius]; for (qint32 i = 0; i < (rect.width() + 2 * m_xRadius); i++) max[i] = m_yRadius + 2; max += m_xRadius; for (qint32 i = 0; i < 3; i++) buf[i] = new quint8[rect.width()]; transition = new quint8*[m_yRadius + 1]; for (qint32 i = 0; i < m_yRadius + 1; i++) { transition[i] = new quint8[rect.width() + 2 * m_xRadius]; memset(transition[i], 0, rect.width() + 2 * m_xRadius); transition[i] += m_xRadius; } quint8* out = new quint8[rect.width()]; density = new quint8*[2 * m_xRadius + 1]; density += m_xRadius; for (qint32 x = 0; x < (m_xRadius + 1); x++) { // allocate density[][] density[ x] = new quint8[2 * m_yRadius + 1]; density[ x] += m_yRadius; density[-x] = density[x]; } for (qint32 x = 0; x < (m_xRadius + 1); x++) { // compute density[][] double tmpx, tmpy, dist; quint8 a; tmpx = x > 0.0 ? x - 0.5 : 0.0; for (qint32 y = 0; y < (m_yRadius + 1); y++) { tmpy = y > 0.0 ? y - 0.5 : 0.0; dist = ((tmpy * tmpy) / (m_yRadius * m_yRadius) + (tmpx * tmpx) / (m_xRadius * m_xRadius)); if (dist < 1.0) a = (quint8)(255 * (1.0 - sqrt(dist))); else a = 0; density[ x][ y] = a; density[ x][-y] = a; density[-x][ y] = a; density[-x][-y] = a; } } pixelSelection->readBytes(buf[0], rect.x(), rect.y(), rect.width(), 1); memcpy(buf[1], buf[0], rect.width()); if (rect.height() > 1) pixelSelection->readBytes(buf[2], rect.x(), rect.y() + 1, rect.width(), 1); else memcpy(buf[2], buf[1], rect.width()); computeTransition(transition[1], buf, rect.width()); for (qint32 y = 1; y < m_yRadius && y + 1 < rect.height(); y++) { // set up top of image rotatePointers(buf, 3); pixelSelection->readBytes(buf[2], rect.x(), rect.y() + y + 1, rect.width(), 1); computeTransition(transition[y + 1], buf, rect.width()); } for (qint32 x = 0; x < rect.width(); x++) { // set up max[] for top of image max[x] = -(m_yRadius + 7); for (qint32 j = 1; j < m_yRadius + 1; j++) if (transition[j][x]) { max[x] = j; break; } } for (qint32 y = 0; y < rect.height(); y++) { // main calculation loop rotatePointers(buf, 3); rotatePointers(transition, m_yRadius + 1); if (y < rect.height() - (m_yRadius + 1)) { pixelSelection->readBytes(buf[2], rect.x(), rect.y() + y + m_yRadius + 1, rect.width(), 1); computeTransition(transition[m_yRadius], buf, rect.width()); } else memcpy(transition[m_yRadius], transition[m_yRadius - 1], rect.width()); for (qint32 x = 0; x < rect.width(); x++) { // update max array if (max[x] < 1) { if (max[x] <= -m_yRadius) { if (transition[m_yRadius][x]) max[x] = m_yRadius; else max[x]--; } else if (transition[-max[x]][x]) max[x] = -max[x]; else if (transition[-max[x] + 1][x]) max[x] = -max[x] + 1; else max[x]--; } else max[x]--; if (max[x] < -m_yRadius - 1) max[x] = -m_yRadius - 1; } quint8 last_max = max[0][density[-1]]; qint32 last_index = 1; for (qint32 x = 0 ; x < rect.width(); x++) { // render scan line last_index--; if (last_index >= 0) { last_max = 0; for (qint32 i = m_xRadius; i >= 0; i--) if (max[x + i] <= m_yRadius && max[x + i] >= -m_yRadius && density[i][max[x+i]] > last_max) { last_max = density[i][max[x + i]]; last_index = i; } out[x] = last_max; } else { last_max = 0; for (qint32 i = m_xRadius; i >= -m_xRadius; i--) if (max[x + i] <= m_yRadius && max[x + i] >= -m_yRadius && density[i][max[x + i]] > last_max) { last_max = density[i][max[x + i]]; last_index = i; } out[x] = last_max; } if (last_max == 0) { qint32 i; for (i = x + 1; i < rect.width(); i++) { if (max[i] >= -m_yRadius) break; } if (i - x > m_xRadius) { for (; x < i - m_xRadius; x++) out[x] = 0; x--; } last_index = m_xRadius; } } pixelSelection->writeBytes(out, rect.x(), rect.y() + y, rect.width(), 1); } delete [] out; for (qint32 i = 0; i < 3; i++) delete[] buf[i]; max -= m_xRadius; delete[] max; for (qint32 i = 0; i < m_yRadius + 1; i++) { transition[i] -= m_xRadius; delete transition[i]; } delete[] transition; for (qint32 i = 0; i < m_xRadius + 1 ; i++) { density[i] -= m_yRadius; delete density[i]; } density -= m_xRadius; delete[] density; } KisFeatherSelectionFilter::KisFeatherSelectionFilter(qint32 radius) : m_radius(radius) { } KUndo2MagicString KisFeatherSelectionFilter::name() { return kundo2_i18n("Feather Selection"); } QRect KisFeatherSelectionFilter::changeRect(const QRect& rect) { return rect.adjusted(-m_radius, -m_radius, m_radius, m_radius); } void KisFeatherSelectionFilter::process(KisPixelSelectionSP pixelSelection, const QRect& rect) { // compute horizontal kernel const uint kernelSize = m_radius * 2 + 1; Eigen::Matrix gaussianMatrix(1, kernelSize); const qreal multiplicand = 1.0 / (2.0 * M_PI * m_radius * m_radius); const qreal exponentMultiplicand = 1.0 / (2.0 * m_radius * m_radius); for (uint x = 0; x < kernelSize; x++) { uint xDistance = qAbs((int)m_radius - (int)x); gaussianMatrix(0, x) = multiplicand * exp( -(qreal)((xDistance * xDistance) + (m_radius * m_radius)) * exponentMultiplicand ); } KisConvolutionKernelSP kernelHoriz = KisConvolutionKernel::fromMatrix(gaussianMatrix, 0, gaussianMatrix.sum()); KisConvolutionKernelSP kernelVertical = KisConvolutionKernel::fromMatrix(gaussianMatrix.transpose(), 0, gaussianMatrix.sum()); KisPaintDeviceSP interm = new KisPaintDevice(pixelSelection->colorSpace()); KisConvolutionPainter horizPainter(interm); horizPainter.setChannelFlags(interm->colorSpace()->channelFlags(false, true)); horizPainter.applyMatrix(kernelHoriz, pixelSelection, rect.topLeft(), rect.topLeft(), rect.size(), BORDER_REPEAT); horizPainter.end(); KisConvolutionPainter verticalPainter(pixelSelection); verticalPainter.setChannelFlags(pixelSelection->colorSpace()->channelFlags(false, true)); verticalPainter.applyMatrix(kernelVertical, interm, rect.topLeft(), rect.topLeft(), rect.size(), BORDER_REPEAT); verticalPainter.end(); } KisGrowSelectionFilter::KisGrowSelectionFilter(qint32 xRadius, qint32 yRadius) : m_xRadius(xRadius), m_yRadius(yRadius) { } KUndo2MagicString KisGrowSelectionFilter::name() { return kundo2_i18n("Grow Selection"); } QRect KisGrowSelectionFilter::changeRect(const QRect& rect) { return rect.adjusted(-m_xRadius, -m_yRadius, m_xRadius, m_yRadius); } void KisGrowSelectionFilter::process(KisPixelSelectionSP pixelSelection, const QRect& rect) { if (m_xRadius <= 0 || m_yRadius <= 0) return; /** * Much code resembles Shrink filter, so please fix bugs * in both filters */ quint8 **buf; // caches the region's pixel data quint8 **max; // caches the largest values for each column max = new quint8* [rect.width() + 2 * m_xRadius]; buf = new quint8* [m_yRadius + 1]; for (qint32 i = 0; i < m_yRadius + 1; i++) { buf[i] = new quint8[rect.width()]; } quint8* buffer = new quint8[(rect.width() + 2 * m_xRadius) *(m_yRadius + 1)]; for (qint32 i = 0; i < rect.width() + 2 * m_xRadius; i++) { if (i < m_xRadius) max[i] = buffer; else if (i < rect.width() + m_xRadius) max[i] = &buffer[(m_yRadius + 1) * (i - m_xRadius)]; else max[i] = &buffer[(m_yRadius + 1) * (rect.width() + m_xRadius - 1)]; for (qint32 j = 0; j < m_xRadius + 1; j++) max[i][j] = 0; } /* offset the max pointer by m_xRadius so the range of the array is [-m_xRadius] to [region->w + m_xRadius] */ max += m_xRadius; quint8* out = new quint8[ rect.width()]; // holds the new scan line we are computing qint32* circ = new qint32[ 2 * m_xRadius + 1 ]; // holds the y coords of the filter's mask computeBorder(circ, m_xRadius, m_yRadius); /* offset the circ pointer by m_xRadius so the range of the array is [-m_xRadius] to [m_xRadius] */ circ += m_xRadius; memset(buf[0], 0, rect.width()); for (qint32 i = 0; i < m_yRadius && i < rect.height(); i++) { // load top of image pixelSelection->readBytes(buf[i + 1], rect.x(), rect.y() + i, rect.width(), 1); } for (qint32 x = 0; x < rect.width() ; x++) { // set up max for top of image max[x][0] = 0; // buf[0][x] is always 0 max[x][1] = buf[1][x]; // MAX (buf[1][x], max[x][0]) always = buf[1][x] for (qint32 j = 2; j < m_yRadius + 1; j++) { max[x][j] = MAX(buf[j][x], max[x][j-1]); } } for (qint32 y = 0; y < rect.height(); y++) { rotatePointers(buf, m_yRadius + 1); if (y < rect.height() - (m_yRadius)) pixelSelection->readBytes(buf[m_yRadius], rect.x(), rect.y() + y + m_yRadius, rect.width(), 1); else memset(buf[m_yRadius], 0, rect.width()); for (qint32 x = 0; x < rect.width(); x++) { /* update max array */ for (qint32 i = m_yRadius; i > 0; i--) { max[x][i] = MAX(MAX(max[x][i - 1], buf[i - 1][x]), buf[i][x]); } max[x][0] = buf[0][x]; } qint32 last_max = max[0][circ[-1]]; qint32 last_index = 1; for (qint32 x = 0; x < rect.width(); x++) { /* render scan line */ last_index--; if (last_index >= 0) { if (last_max == 255) out[x] = 255; else { last_max = 0; for (qint32 i = m_xRadius; i >= 0; i--) if (last_max < max[x + i][circ[i]]) { last_max = max[x + i][circ[i]]; last_index = i; } out[x] = last_max; } } else { last_index = m_xRadius; last_max = max[x + m_xRadius][circ[m_xRadius]]; for (qint32 i = m_xRadius - 1; i >= -m_xRadius; i--) if (last_max < max[x + i][circ[i]]) { last_max = max[x + i][circ[i]]; last_index = i; } out[x] = last_max; } } pixelSelection->writeBytes(out, rect.x(), rect.y() + y, rect.width(), 1); } - /* undo the offsets to the pointers so we can free the malloced memmory */ + /* undo the offsets to the pointers so we can free the malloced memory */ circ -= m_xRadius; max -= m_xRadius; delete[] circ; delete[] buffer; delete[] max; for (qint32 i = 0; i < m_yRadius + 1; i++) delete[] buf[i]; delete[] buf; delete[] out; } KisShrinkSelectionFilter::KisShrinkSelectionFilter(qint32 xRadius, qint32 yRadius, bool edgeLock) : m_xRadius(xRadius), m_yRadius(yRadius), m_edgeLock(edgeLock) { } KUndo2MagicString KisShrinkSelectionFilter::name() { return kundo2_i18n("Shrink Selection"); } QRect KisShrinkSelectionFilter::changeRect(const QRect& rect) { return rect.adjusted(-m_xRadius, -m_yRadius, m_xRadius, m_yRadius); } void KisShrinkSelectionFilter::process(KisPixelSelectionSP pixelSelection, const QRect& rect) { if (m_xRadius <= 0 || m_yRadius <= 0) return; /* pretty much the same as fatten_region only different blame all bugs in this function on jaycox@gimp.org */ /* If edge_lock is true we assume that pixels outside the region we are passed are identical to the edge pixels. If edge_lock is false, we assume that pixels outside the region are 0 */ quint8 **buf; // caches the region's pixels quint8 **max; // caches the smallest values for each column qint32 last_max, last_index; max = new quint8* [rect.width() + 2 * m_xRadius]; buf = new quint8* [m_yRadius + 1]; for (qint32 i = 0; i < m_yRadius + 1; i++) { buf[i] = new quint8[rect.width()]; } qint32 buffer_size = (rect.width() + 2 * m_xRadius + 1) * (m_yRadius + 1); quint8* buffer = new quint8[buffer_size]; if (m_edgeLock) memset(buffer, 255, buffer_size); else memset(buffer, 0, buffer_size); for (qint32 i = 0; i < rect.width() + 2 * m_xRadius; i++) { if (i < m_xRadius) if (m_edgeLock) max[i] = buffer; else max[i] = &buffer[(m_yRadius + 1) * (rect.width() + m_xRadius)]; else if (i < rect.width() + m_xRadius) max[i] = &buffer[(m_yRadius + 1) * (i - m_xRadius)]; else if (m_edgeLock) max[i] = &buffer[(m_yRadius + 1) * (rect.width() + m_xRadius - 1)]; else max[i] = &buffer[(m_yRadius + 1) * (rect.width() + m_xRadius)]; } if (!m_edgeLock) for (qint32 j = 0 ; j < m_xRadius + 1; j++) max[0][j] = 0; // offset the max pointer by m_xRadius so the range of the array is [-m_xRadius] to [region->w + m_xRadius] max += m_xRadius; quint8* out = new quint8[rect.width()]; // holds the new scan line we are computing qint32* circ = new qint32[2 * m_xRadius + 1]; // holds the y coords of the filter's mask computeBorder(circ, m_xRadius, m_yRadius); // offset the circ pointer by m_xRadius so the range of the array is [-m_xRadius] to [m_xRadius] circ += m_xRadius; for (qint32 i = 0; i < m_yRadius && i < rect.height(); i++) // load top of image pixelSelection->readBytes(buf[i + 1], rect.x(), rect.y() + i, rect.width(), 1); if (m_edgeLock) memcpy(buf[0], buf[1], rect.width()); else memset(buf[0], 0, rect.width()); for (qint32 x = 0; x < rect.width(); x++) { // set up max for top of image max[x][0] = buf[0][x]; for (qint32 j = 1; j < m_yRadius + 1; j++) max[x][j] = MIN(buf[j][x], max[x][j-1]); } for (qint32 y = 0; y < rect.height(); y++) { rotatePointers(buf, m_yRadius + 1); if (y < rect.height() - m_yRadius) pixelSelection->readBytes(buf[m_yRadius], rect.x(), rect.y() + y + m_yRadius, rect.width(), 1); else if (m_edgeLock) memcpy(buf[m_yRadius], buf[m_yRadius - 1], rect.width()); else memset(buf[m_yRadius], 0, rect.width()); for (qint32 x = 0 ; x < rect.width(); x++) { // update max array for (qint32 i = m_yRadius; i > 0; i--) { max[x][i] = MIN(MIN(max[x][i - 1], buf[i - 1][x]), buf[i][x]); } max[x][0] = buf[0][x]; } last_max = max[0][circ[-1]]; last_index = 0; for (qint32 x = 0 ; x < rect.width(); x++) { // render scan line last_index--; if (last_index >= 0) { if (last_max == 0) out[x] = 0; else { last_max = 255; for (qint32 i = m_xRadius; i >= 0; i--) if (last_max > max[x + i][circ[i]]) { last_max = max[x + i][circ[i]]; last_index = i; } out[x] = last_max; } } else { last_index = m_xRadius; last_max = max[x + m_xRadius][circ[m_xRadius]]; for (qint32 i = m_xRadius - 1; i >= -m_xRadius; i--) if (last_max > max[x + i][circ[i]]) { last_max = max[x + i][circ[i]]; last_index = i; } out[x] = last_max; } } pixelSelection->writeBytes(out, rect.x(), rect.y() + y, rect.width(), 1); } - // undo the offsets to the pointers so we can free the malloced memmory + // undo the offsets to the pointers so we can free the malloced memory circ -= m_xRadius; max -= m_xRadius; delete[] circ; delete[] buffer; delete[] max; for (qint32 i = 0; i < m_yRadius + 1; i++) delete[] buf[i]; delete[] buf; delete[] out; } KUndo2MagicString KisSmoothSelectionFilter::name() { return kundo2_i18n("Smooth Selection"); } QRect KisSmoothSelectionFilter::changeRect(const QRect& rect) { const qint32 radius = 1; return rect.adjusted(-radius, -radius, radius, radius); } void KisSmoothSelectionFilter::process(KisPixelSelectionSP pixelSelection, const QRect& rect) { // Simple convolution filter to smooth a mask (1bpp) quint8 *buf[3]; qint32 width = rect.width(); qint32 height = rect.height(); quint8* out = new quint8[width]; for (qint32 i = 0; i < 3; i++) buf[i] = new quint8[width + 2]; // load top of image pixelSelection->readBytes(buf[0] + 1, rect.x(), rect.y(), width, 1); buf[0][0] = buf[0][1]; buf[0][width + 1] = buf[0][width]; memcpy(buf[1], buf[0], width + 2); for (qint32 y = 0; y < height; y++) { if (y + 1 < height) { pixelSelection->readBytes(buf[2] + 1, rect.x(), rect.y() + y + 1, width, 1); buf[2][0] = buf[2][1]; buf[2][width + 1] = buf[2][width]; } else { memcpy(buf[2], buf[1], width + 2); } for (qint32 x = 0 ; x < width; x++) { qint32 value = (buf[0][x] + buf[0][x+1] + buf[0][x+2] + buf[1][x] + buf[2][x+1] + buf[1][x+2] + buf[2][x] + buf[1][x+1] + buf[2][x+2]); out[x] = value / 9; } pixelSelection->writeBytes(out, rect.x(), rect.y() + y, width, 1); rotatePointers(buf, 3); } for (qint32 i = 0; i < 3; i++) delete[] buf[i]; delete[] out; } KUndo2MagicString KisInvertSelectionFilter::name() { return kundo2_i18n("Invert Selection"); } QRect KisInvertSelectionFilter::changeRect(const QRect& rect) { Q_UNUSED(rect); return QRect(); } void KisInvertSelectionFilter::process(KisPixelSelectionSP pixelSelection, const QRect& rect) { Q_UNUSED(rect); pixelSelection->invert(); } diff --git a/libs/image/kis_transform_worker.cc b/libs/image/kis_transform_worker.cc index b0a4dc67df..ea23f08d31 100644 --- a/libs/image/kis_transform_worker.cc +++ b/libs/image/kis_transform_worker.cc @@ -1,684 +1,684 @@ /* * Copyright (c) 2004 Michael Thaler filters * Copyright (c) 2005-2007 C. Boemann * Copyright (c) 2005, 2010 Boudewijn Rempt * Copyright (c) 2010 Marc Pegon * Copyright (c) 2013 Dmitry Kazakov * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_transform_worker.h" #include #include #include #include #include #include #include "kis_paint_device.h" #include "kis_debug.h" #include "kis_selection.h" #include "kis_iterator_ng.h" #include "kis_random_accessor_ng.h" #include "kis_filter_strategy.h" #include "kis_painter.h" #include "kis_filter_weights_applicator.h" #include "kis_progress_update_helper.h" #include "kis_pixel_selection.h" #include "kis_image.h" KisTransformWorker::KisTransformWorker(KisPaintDeviceSP dev, double xscale, double yscale, double xshear, double yshear, double xshearOrigin, double yshearOrigin, double rotation, qint32 xtranslate, qint32 ytranslate, KoUpdaterPtr progress, KisFilterStrategy *filter) { m_dev = dev; m_xscale = xscale; m_yscale = yscale; m_xshear = xshear; m_yshear = yshear; m_xshearOrigin = xshearOrigin; m_yshearOrigin = yshearOrigin; m_rotation = rotation, m_xtranslate = xtranslate; m_ytranslate = ytranslate; m_progressUpdater = progress; m_filter = filter; } KisTransformWorker::~KisTransformWorker() { } QTransform KisTransformWorker::transform() const { QTransform TS = QTransform::fromTranslate(m_xshearOrigin, m_yshearOrigin); QTransform S; S.shear(0, m_yshear); S.shear(m_xshear, 0); QTransform SC = QTransform::fromScale(m_xscale, m_yscale); QTransform R; R.rotateRadians(m_rotation); QTransform T = QTransform::fromTranslate(m_xtranslate, m_ytranslate); return TS.inverted() * S * TS * SC * R * T; } void KisTransformWorker::transformPixelSelectionOutline(KisPixelSelectionSP pixelSelection) const { if (pixelSelection->outlineCacheValid()) { QPainterPath outlineCache = pixelSelection->outlineCache(); pixelSelection->setOutlineCache(transform().map(outlineCache)); } } QRect rotateWithTf(int rotation, KisPaintDeviceSP dev, QRect boundRect, KoUpdaterPtr progressUpdater, int portion) { qint32 pixelSize = dev->pixelSize(); QRect r(boundRect); KisPaintDeviceSP tmp = new KisPaintDevice(dev->colorSpace()); tmp->prepareClone(dev); KisRandomAccessorSP devAcc = dev->createRandomAccessorNG(0, 0); KisRandomAccessorSP tmpAcc = tmp->createRandomAccessorNG(0, 0); KisProgressUpdateHelper progressHelper(progressUpdater, portion, r.height()); QTransform tf; tf = tf.rotate(rotation); int ty = 0; int tx = 0; for (qint32 y = r.y(); y <= r.height() + r.y(); ++y) { for (qint32 x = r.x(); x <= r.width() + r.x(); ++x) { tf.map(x, y, &tx, &ty); devAcc->moveTo(x, y); tmpAcc->moveTo(tx, ty); memcpy(tmpAcc->rawData(), devAcc->rawData(), pixelSize); } progressHelper.step(); } dev->makeCloneFrom(tmp, tmp->region().boundingRect()); return r; } QRect KisTransformWorker::rotateRight90(KisPaintDeviceSP dev, QRect boundRect, KoUpdaterPtr progressUpdater, int portion) { QRect r = rotateWithTf(90, dev, boundRect, progressUpdater, portion); dev->moveTo(dev->x() - 1, dev->y()); return QRect(- r.top() - r.height(), r.x(), r.height(), r.width()); } QRect KisTransformWorker::rotateLeft90(KisPaintDeviceSP dev, QRect boundRect, KoUpdaterPtr progressUpdater, int portion) { QRect r = rotateWithTf(270, dev, boundRect, progressUpdater, portion); dev->moveTo(dev->x(), dev->y() - 1); return QRect(r.top(), - r.x() - r.width(), r.height(), r.width()); } QRect KisTransformWorker::rotate180(KisPaintDeviceSP dev, QRect boundRect, KoUpdaterPtr progressUpdater, int portion) { QRect r = rotateWithTf(180, dev, boundRect, progressUpdater, portion); dev->moveTo(dev->x() - 1, dev->y() -1); return QRect(- r.x() - r.width(), - r.top() - r.height(), r.width(), r.height()); } template void calcDimensions(QRect rc, qint32 &srcStart, qint32 &srcLen, qint32 &firstLine, qint32 &numLines); template <> void calcDimensions (QRect rc, qint32 &srcStart, qint32 &srcLen, qint32 &firstLine, qint32 &numLines) { srcStart = rc.x(); srcLen = rc.width(); firstLine = rc.y(); numLines = rc.height(); } template <> void calcDimensions (QRect rc, qint32 &srcStart, qint32 &srcLen, qint32 &firstLine, qint32 &numLines) { srcStart = rc.y(); srcLen = rc.height(); firstLine = rc.x(); numLines = rc.width(); } template void updateBounds(QRect &boundRect, const KisFilterWeightsApplicator::LinePos &newBounds); template <> void updateBounds(QRect &boundRect, const KisFilterWeightsApplicator::LinePos &newBounds) { boundRect.setLeft(newBounds.start()); boundRect.setWidth(newBounds.size()); } template <> void updateBounds(QRect &boundRect, const KisFilterWeightsApplicator::LinePos &newBounds) { boundRect.setTop(newBounds.start()); boundRect.setHeight(newBounds.size()); } template void KisTransformWorker::transformPass(KisPaintDevice *src, KisPaintDevice *dst, double floatscale, double shear, double dx, KisFilterStrategy *filterStrategy, int portion) { bool clampToEdge = shear == 0.0; qint32 srcStart, srcLen, firstLine, numLines; calcDimensions(m_boundRect, srcStart, srcLen, firstLine, numLines); KisProgressUpdateHelper progressHelper(m_progressUpdater, portion, numLines); KisFilterWeightsBuffer buf(filterStrategy, qAbs(floatscale)); KisFilterWeightsApplicator applicator(src, dst, floatscale, shear, dx, clampToEdge); KisFilterWeightsApplicator::LinePos dstBounds; for (int i = firstLine; i < firstLine + numLines; i++) { KisFilterWeightsApplicator::LinePos dstPos; KisFilterWeightsApplicator::LinePos srcPos(srcStart, srcLen); dstPos = applicator.processLine(srcPos, i, &buf, filterStrategy->support()); dstBounds.unite(dstPos); progressHelper.step(); } updateBounds(m_boundRect, dstBounds); } template void swapValues(T *a, T *b) { T c = *a; *a = *b; *b = c; } bool KisTransformWorker::run() { return runPartial(m_dev->exactBounds()); } bool KisTransformWorker::runPartial(const QRect &processRect) { /* Check for nonsense and let the user know, this helps debugging. Otherwise the program will crash at a later point, in a very obscure way, probably by division by zero */ Q_ASSERT_X(m_xscale != 0, "KisTransformer::run() validation step", "xscale == 0"); Q_ASSERT_X(m_yscale != 0, "KisTransformer::run() validation step", "yscale == 0"); // Fallback safety line in case Krita is compiled without ASSERTS if (m_xscale == 0 || m_yscale == 0) return false; m_boundRect = processRect; if (m_boundRect.isNull()) { if (!m_progressUpdater.isNull()) { m_progressUpdater->setProgress(100); } return true; } double xscale = m_xscale; double yscale = m_yscale; double rotation = m_rotation; qint32 xtranslate = m_xtranslate; qint32 ytranslate = m_ytranslate; // Apply shearX/Y separately. In Krita it is demanded separately // most of the times. if (m_xshear != 0 || m_yshear != 0) { int portion = 50; int dx = - qRound(m_yshearOrigin * yscale * m_xshear); int dy = - qRound(m_xshearOrigin * xscale * m_yshear); bool scalePresent = !(qFuzzyCompare(xscale, 1.0) && qFuzzyCompare(yscale, 1.0)); bool xShearPresent = !qFuzzyCompare(m_xshear, 0.0); bool yShearPresent = !qFuzzyCompare(m_yshear, 0.0); if (scalePresent || (xShearPresent && yShearPresent)) { transformPass (m_dev.data(), m_dev.data(), xscale, yscale * m_xshear, dx, m_filter, portion); transformPass (m_dev.data(), m_dev.data(), yscale, m_yshear, dy, m_filter, portion); } else if (xShearPresent) { transformPass (m_dev.data(), m_dev.data(), xscale, m_xshear, dx, m_filter, portion); m_boundRect.translate(0, dy); m_dev->moveTo(m_dev->x(), m_dev->y() + dy); } else if (yShearPresent) { transformPass (m_dev.data(), m_dev.data(), yscale, m_yshear, dy, m_filter, portion); m_boundRect.translate(dx, 0); m_dev->moveTo(m_dev->x() + dx, m_dev->y()); } yscale = 1.; xscale = 1.; } if (rotation < 0.0) { rotation = -fmod(-rotation, 2 * M_PI) + 2 * M_PI; } else { rotation = fmod(rotation, 2 * M_PI); } int rotQuadrant = int(rotation / (M_PI / 2) + 0.5) & 3; rotation -= rotQuadrant * M_PI / 2; bool simpleTranslation = qFuzzyCompare(rotation, 0.0) && qFuzzyCompare(xscale, 1.0) && qFuzzyCompare(yscale, 1.0); int progressTotalSteps = qMax(1, 2 * (!simpleTranslation) + (rotQuadrant != 0)); int progressPortion = 100 / progressTotalSteps; /** * Pre-rotate the image to ensure the actual resampling is done * for an angle -pi/4...pi/4. This is faster and produces better * quality. */ switch (rotQuadrant) { case 1: swapValues(&xscale, &yscale); m_boundRect = rotateRight90(m_dev, m_boundRect, m_progressUpdater, progressPortion); break; case 2: m_boundRect = rotate180(m_dev, m_boundRect, m_progressUpdater, progressPortion); break; case 3: swapValues(&xscale, &yscale); m_boundRect = rotateLeft90(m_dev, m_boundRect, m_progressUpdater, progressPortion); break; default: /* do nothing */ break; } if (simpleTranslation) { m_boundRect.translate(xtranslate, ytranslate); m_dev->moveTo(m_dev->x() + xtranslate, m_dev->y() + ytranslate); } else { QTransform SC = QTransform::fromScale(xscale, yscale); QTransform R; R.rotateRadians(rotation); QTransform T = QTransform::fromTranslate(xtranslate, ytranslate); QTransform m = SC * R * T; /** * First X-pass, then Y-pass * | a 0 0 | | 1 d 0 | * m = | b 1 0 | x | 0 e 0 | (matrices are in Qt's notation) * | c 0 1 | | 0 f 1 | */ qreal a = m.m11(); qreal b = m.m21(); qreal c = m.m31(); qreal d = m.m12() / m.m11(); qreal e = m.m22() - m.m21() * m.m12() / m.m11(); qreal f = m.m32() - m.m31() * m.m12() / m.m11(); // First Pass (X) transformPass (m_dev.data(), m_dev.data(), a, b, c, m_filter, progressPortion); // Second Pass (Y) transformPass (m_dev.data(), m_dev.data(), e, d, f, m_filter, progressPortion); #if 0 /************************************************************/ /** * First Y-pass, then X-pass (for testing purposes) * | 1 d 0 | | a 0 0 | * m = | 0 e 0 | x | b 1 0 | (matrices are in Qt's notation) * | 0 f 1 | | c 0 1 | */ qreal a = m.m11() - m.m21() * m.m12() / m.m22(); qreal b = m.m21() / m.m22(); qreal c = m.m31() - m.m21() * m.m32() / m.m22(); qreal d = m.m12(); qreal e = m.m22(); qreal f = m.m32(); // First Pass (X) transformPass (m_dev.data(), m_dev.data(), a, b, c, m_filter, progressPortion); // Second Pass (Y) transformPass (m_dev.data(), m_dev.data(), e, d, f, m_filter, progressPortion); /************************************************************/ #endif /* 0 */ #if 0 /************************************************************/ // Old three-pass implementation (for testing purposes) yshear = sin(rotation); xshear = -tan(rotation / 2); xtranslate -= int(xshear * ytranslate); transformPass (m_dev.data(), m_dev.data(), xscale, yscale*xshear, 0, m_filter, 0); transformPass (m_dev.data(), m_dev.data(), yscale, yshear, ytranslate, m_filter, 0); if (xshear != 0.0) { transformPass (m_dev.data(), m_dev.data(), 1.0, xshear, xtranslate, m_filter, 0); } else { m_dev->move(m_dev->x() + xtranslate, m_dev->y()); updateBounds (m_boundRect, 1.0, 0, xtranslate); } /************************************************************/ #endif /* 0 */ } if (!m_progressUpdater.isNull()) { m_progressUpdater->setProgress(100); } /** * Purge the tiles which might be left after scaling down the * image */ m_dev->purgeDefaultPixels(); return true; } void mirror_impl(KisPaintDeviceSP dev, qreal axis, bool isHorizontal) { KIS_ASSERT_RECOVER_RETURN(qFloor(axis) == axis || (axis - qFloor(axis) == 0.5)); QRect mirrorRect = dev->exactBounds(); if (mirrorRect.width() <= 1) return; /** * We split the total mirror rect into two halves, which lay to * the 'left' and 'right' from the axis. Effectively, these halves * should be swapped, but there is a bit of optimization: some * parts of these portions overlap and some don't. So former ones * should be really swapped, but the latter ones just moved to the * other side. * * So the algorithm consists of two stages: * * 1) Move the non-overlapping portion of the mirror rect to the * other side of the axis. The move may be either left-to-right or * right-to-left. * * 2) Use slow 'swap' operation for the remaining portion of the * mirrorRect. * * NOTE: the algorithm works with (column, row) coordinates which * are mapped to the real (x, y) depending on the value of * 'isHorizontal' parameter. */ int leftStart; int rightEnd; if (isHorizontal) { leftStart = mirrorRect.x(); rightEnd = mirrorRect.x() + mirrorRect.width(); } else { leftStart = mirrorRect.y(); rightEnd = mirrorRect.y() + mirrorRect.height(); } /** - * If the axis is not aligned, that is crosses some pixel cell, we shoudl just skip this + * If the axis is not aligned, that is crosses some pixel cell, we should just skip this * column and not process it. Actually, how can we mirror the central single-pixel column? */ const bool axisNonAligned = qFloor(axis) < axis; int leftCenterPoint = qFloor(axis); int leftEnd = qMin(leftCenterPoint, rightEnd); int rightCenterPoint = axisNonAligned ? qCeil(axis) : qFloor(axis); int rightStart = qMax(rightCenterPoint, leftStart); int leftSize = qMax(0, leftEnd - leftStart); int rightSize = qMax(0, rightEnd - rightStart); int maxDistanceToAxis = qMax(leftCenterPoint - leftStart, rightEnd - rightCenterPoint); // Main variables for controlling the stages of the algorithm bool moveLeftToRight = leftSize > rightSize; int moveAmount = qAbs(leftSize - rightSize); int swapAmount = qMin(leftSize, rightSize); // Initial position of 'left' and 'right' block iterators int initialLeftCol = leftCenterPoint - maxDistanceToAxis; int initialRightCol = rightCenterPoint + maxDistanceToAxis - 1; KisRandomAccessorSP leftIt = dev->createRandomAccessorNG(mirrorRect.x(), mirrorRect.y()); KisRandomAccessorSP rightIt = dev->createRandomAccessorNG(mirrorRect.x(), mirrorRect.y()); const KoColor defaultPixelObject = dev->defaultPixel(); const quint8 *defaultPixel = defaultPixelObject.data(); const int pixelSize = dev->pixelSize(); QByteArray buf(pixelSize, 0); // Map (column, row) -> (x, y) int rowsRemaining; int row; if (isHorizontal) { rowsRemaining = mirrorRect.height(); row = mirrorRect.y(); } else { rowsRemaining = mirrorRect.width(); row = mirrorRect.x(); } int leftColPos = 0; int rightColPos = 0; const int &leftX = isHorizontal ? leftColPos : row; const int &leftY = isHorizontal ? row : leftColPos; const int &rightX = isHorizontal ? rightColPos : row; const int &rightY = isHorizontal ? row : rightColPos; while (rowsRemaining) { leftColPos = initialLeftCol; rightColPos = initialRightCol; int rows = qMin(rowsRemaining, isHorizontal ? leftIt->numContiguousRows(leftY) : leftIt->numContiguousColumns(leftX)); int rowStride = isHorizontal ? leftIt->rowStride(leftX, leftY) : pixelSize; if (moveLeftToRight) { for (int i = 0; i < moveAmount; i++) { leftIt->moveTo(leftX, leftY); rightIt->moveTo(rightX, rightY); quint8 *leftPtr = leftIt->rawData(); quint8 *rightPtr = rightIt->rawData(); for (int j = 0; j < rows; j++) { // left-to-right move memcpy(rightPtr, leftPtr, pixelSize); memcpy(leftPtr, defaultPixel, pixelSize); leftPtr += rowStride; rightPtr += rowStride; } leftColPos++; rightColPos--; } } else { for (int i = 0; i < moveAmount; i++) { leftIt->moveTo(leftX, leftY); rightIt->moveTo(rightX, rightY); quint8 *leftPtr = leftIt->rawData(); quint8 *rightPtr = rightIt->rawData(); for (int j = 0; j < rows; j++) { // right-to-left move memcpy(leftPtr, rightPtr, pixelSize); memcpy(rightPtr, defaultPixel, pixelSize); leftPtr += rowStride; rightPtr += rowStride; } leftColPos++; rightColPos--; } } for (int i = 0; i < swapAmount; i++) { leftIt->moveTo(leftX, leftY); rightIt->moveTo(rightX, rightY); quint8 *leftPtr = leftIt->rawData(); quint8 *rightPtr = rightIt->rawData(); for (int j = 0; j < rows; j++) { // swap operation memcpy(buf.data(), leftPtr, pixelSize); memcpy(leftPtr, rightPtr, pixelSize); memcpy(rightPtr, buf.data(), pixelSize); leftPtr += rowStride; rightPtr += rowStride; } leftColPos++; rightColPos--; } rowsRemaining -= rows; row += rows; } } void KisTransformWorker::mirrorX(KisPaintDeviceSP dev, qreal axis) { mirror_impl(dev, axis, true); } void KisTransformWorker::mirrorY(KisPaintDeviceSP dev, qreal axis) { mirror_impl(dev, axis, false); } void KisTransformWorker::mirrorX(KisPaintDeviceSP dev) { QRect bounds = dev->exactBounds(); mirrorX(dev, bounds.x() + 0.5 * bounds.width()); } void KisTransformWorker::mirrorY(KisPaintDeviceSP dev) { QRect bounds = dev->exactBounds(); mirrorY(dev, bounds.y() + 0.5 * bounds.height()); } void KisTransformWorker::mirror(KisPaintDeviceSP dev, qreal axis, Qt::Orientation orientation) { mirror_impl(dev, axis, orientation == Qt::Horizontal); } void KisTransformWorker::offset(KisPaintDeviceSP device, const QPoint& offsetPosition, const QRect& wrapRect) { Q_ASSERT(wrapRect == wrapRect.normalized()); // inspired by gimp offset code, only wrap mode supported int sx = wrapRect.x(); int sy = wrapRect.y(); int width = wrapRect.width(); int height = wrapRect.height(); // offset coords are relative to space wrapRect int offsetX = offsetPosition.x(); int offsetY = offsetPosition.y(); while (offsetX < 0) { offsetX += width; } while (offsetY < 0) { offsetY += height; } if ((offsetX == 0) && (offsetY == 0)) { return; } KisPaintDeviceSP offsetDevice = new KisPaintDevice(device->colorSpace()); int srcX = 0; int srcY = 0; int destX = offsetX; int destY = offsetY; width = qBound(0, width - offsetX, width); height = qBound(0, height - offsetY, height); if ((width != 0) && (height != 0)) { // convert back to paint device space KisPainter::copyAreaOptimized(QPoint(destX + sx, destY + sy), device, offsetDevice, QRect(srcX + sx, srcY + sy, width, height)); } srcX = wrapRect.width() - offsetX; srcY = wrapRect.height() - offsetY; destX = (srcX + offsetX) % wrapRect.width(); destY = (srcY + offsetY) % wrapRect.height(); if (offsetX != 0 && offsetY != 0) { KisPainter::copyAreaOptimized(QPoint(destX + sx, destY + sy), device, offsetDevice, QRect(srcX + sx, srcY + sy, offsetX, offsetY)); } if (offsetX != 0) { KisPainter::copyAreaOptimized(QPoint(destX + sx, (destY + offsetY) + sy), device, offsetDevice, QRect(srcX + sx, 0 + sy, offsetX, wrapRect.height() - offsetY)); } if (offsetY != 0) { KisPainter::copyAreaOptimized(QPoint((destX + offsetX) + sx, destY + sy), device, offsetDevice, QRect(0 + sx, srcY + sy, wrapRect.width() - offsetX, offsetY)); } // bitblt the result back QRect resultRect(sx, sy, wrapRect.width(), wrapRect.height()); KisPainter::copyAreaOptimized(resultRect.topLeft(), offsetDevice, device, resultRect); } diff --git a/libs/image/lazybrush/KisWatershedWorker.h b/libs/image/lazybrush/KisWatershedWorker.h index 4e4d4dc5e8..95fd52c714 100644 --- a/libs/image/lazybrush/KisWatershedWorker.h +++ b/libs/image/lazybrush/KisWatershedWorker.h @@ -1,82 +1,82 @@ /* * Copyright (c) 2017 Dmitry Kazakov * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KISWATERSHEDWORKER_H #define KISWATERSHEDWORKER_H #include #include "kis_types.h" #include "kritaimage_export.h" class KoColor; class KRITAIMAGE_EXPORT KisWatershedWorker { public: /** * Creates an empty watershed worker withouth any strokes attached. The strokes * should be attached manually with addKeyStroke() call. * * @param heightMap prefiltered height map in alpha8 colorspace, with "0" meaning * background color and "255" meaning line art. Heightmap is *never* * modified by the worker! * @param dst destination device where the result will be written * @param boundingRect the worker refuses to fill outside the bounding rect, considering * that outer area as having +inf height */ KisWatershedWorker(KisPaintDeviceSP heightMap, KisPaintDeviceSP dst, const QRect &boundingRect, KoUpdater *progress = 0); ~KisWatershedWorker(); /** * @brief Adds a key stroke to the worker. * * The key strokes may intersect, in which case the lastly added stroke will have - * a proirity over all the previous ones. + * a priority over all the previous ones. * * @param dev alpha8 paint device of the key stroke, may contain disjoint areas * @param color the color of the stroke */ void addKeyStroke(KisPaintDeviceSP dev, const KoColor &color); /** * @brief run the filling process using the passes height map, strokes, and write * the result coloring into the destination device * @param cleanUpAmount shows how aggressively we should try to clean up the final * coloring. Should be in range [0.0...1.0] */ void run(qreal cleanUpAmount = 0.0); int testingGroupPositiveEdge(qint32 group, quint8 level); int testingGroupNegativeEdge(qint32 group, quint8 level); int testingGroupForeignEdge(qint32 group, quint8 level); int testingGroupAllyEdge(qint32 group, quint8 level); int testingGroupConflicts(qint32 group, quint8 level, qint32 withGroup); void testingTryRemoveGroup(qint32 group, quint8 level); private: struct Private; const QScopedPointer m_d; }; #endif // KISWATERSHEDWORKER_H diff --git a/libs/image/metadata/kis_meta_data_value.cc b/libs/image/metadata/kis_meta_data_value.cc index 26b7c455f3..39413171b8 100644 --- a/libs/image/metadata/kis_meta_data_value.cc +++ b/libs/image/metadata/kis_meta_data_value.cc @@ -1,454 +1,454 @@ /* * Copyright (c) 2007,2010 Cyrille Berger * * This library is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This library 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_meta_data_value.h" #include #include #include #include #include #include #include #include using namespace KisMetaData; struct Q_DECL_HIDDEN Value::Private { Private() : type(Invalid) {} union { QVariant* variant; QList* array; QMap* structure; KisMetaData::Rational* rational; } value; ValueType type; QMap propertyQualifiers; }; Value::Value() : d(new Private) { d->type = Invalid; } Value::Value(const QVariant& variant) : d(new Private) { d->type = Value::Variant; d->value.variant = new QVariant(variant); } Value::Value(const QList& array, ValueType type) : d(new Private) { Q_ASSERT(type == OrderedArray || type == UnorderedArray || type == AlternativeArray || type == LangArray); d->value.array = new QList(array); d->type = type; // TODO: I am hesitating about LangArray to keep them as array or convert them to maps } Value::Value(const QMap& structure) : d(new Private) { d->type = Structure; d->value.structure = new QMap(structure); } Value::Value(const KisMetaData::Rational& signedRational) : d(new Private) { d->type = Value::Rational; d->value.rational = new KisMetaData::Rational(signedRational); } Value::Value(const Value& v) : d(new Private) { d->type = Invalid; *this = v; } Value& Value::operator=(const Value & v) { d->type = v.d->type; d->propertyQualifiers = v.d->propertyQualifiers; switch (d->type) { case Invalid: break; case Variant: d->value.variant = new QVariant(*v.d->value.variant); break; case OrderedArray: case UnorderedArray: case AlternativeArray: case LangArray: d->value.array = new QList(*v.d->value.array); break; case Structure: d->value.structure = new QMap(*v.d->value.structure); break; case Rational: d->value.rational = new KisMetaData::Rational(*v.d->value.rational); } return *this; } Value::~Value() { delete d; } void Value::addPropertyQualifier(const QString& _name, const Value& _value) { d->propertyQualifiers[_name] = _value; } const QMap& Value::propertyQualifiers() const { return d->propertyQualifiers; } Value::ValueType Value::type() const { return d->type; } double Value::asDouble() const { switch (type()) { case Variant: return d->value.variant->toDouble(0); case Rational: return d->value.rational->numerator / (double)d->value.rational->denominator; default: return 0.0; } return 0.0; } int Value::asInteger() const { switch (type()) { case Variant: return d->value.variant->toInt(0); case Rational: return d->value.rational->numerator / d->value.rational->denominator; default: return 0; } return 0; } QVariant Value::asVariant() const { switch (type()) { case Variant: return *d->value.variant; case Rational: return QVariant(QString("%1 / %2").arg(d->value.rational->numerator).arg(d->value.rational->denominator)); default: break; } return QVariant(); } bool Value::setVariant(const QVariant& variant) { switch (type()) { case KisMetaData::Value::Invalid: *this = KisMetaData::Value(variant); return true; case Rational: { QRegExp rx("([^\\/]*)\\/([^\\/]*)"); rx.indexIn(variant.toString()); } case KisMetaData::Value::Variant: { if (d->value.variant->type() == variant.type()) { *d->value.variant = variant; return true; } } return true; default: break; } return false; } bool Value::setStructureVariant(const QString& fieldNAme, const QVariant& variant) { if (type() == Structure) { return (*d->value.structure)[fieldNAme].setVariant(variant); } return false; } bool Value::setArrayVariant(int index, const QVariant& variant) { if (isArray()) { for (int i = d->value.array->size(); i <= index; ++i) { d->value.array->append(Value()); } (*d->value.array)[index].setVariant(variant); } return false; } KisMetaData::Rational Value::asRational() const { if (d->type == Rational) { return *d->value.rational; } return KisMetaData::Rational(); } QList Value::asArray() const { if (isArray()) { return *d->value.array; } return QList(); } bool Value::isArray() const { return type() == OrderedArray || type() == UnorderedArray || type() == AlternativeArray; } QMap Value::asStructure() const { if (type() == Structure) { return *d->value.structure; } return QMap(); } QDebug operator<<(QDebug debug, const Value &v) { switch (v.type()) { case Value::Invalid: debug.nospace() << "invalid value"; break; case Value::Variant: debug.nospace() << "Variant: " << v.asVariant(); break; case Value::OrderedArray: case Value::UnorderedArray: case Value::AlternativeArray: case Value::LangArray: debug.nospace() << "Array: " << v.asArray(); break; case Value::Structure: debug.nospace() << "Structure: " << v.asStructure(); break; case Value::Rational: debug.nospace() << "Rational: " << v.asRational().numerator << " / " << v.asRational().denominator; break; } return debug.space(); } bool Value::operator==(const Value& rhs) const { if (d->type != rhs.d->type) return false; switch (d->type) { case Value::Invalid: return true; case Value::Variant: return asVariant() == rhs.asVariant(); case Value::OrderedArray: case Value::UnorderedArray: case Value::AlternativeArray: case Value::LangArray: return asArray() == rhs.asArray(); case Value::Structure: return asStructure() == rhs.asStructure(); case Value::Rational: return asRational() == rhs.asRational(); } return false; } Value& Value::operator+=(const Value & v) { switch (d->type) { case Value::Invalid: Q_ASSERT(v.type() == Value::Invalid); break; case Value::Variant: Q_ASSERT(v.type() == Value::Variant); { QVariant v1 = *d->value.variant; QVariant v2 = *v.d->value.variant; Q_ASSERT(v2.canConvert(v1.type())); switch (v1.type()) { default: warnMetaData << "KisMetaData: Merging metadata of type" << v1.type() << "is unsupported!"; break; case QVariant::Date: *d->value.variant = qMax(v1.toDate(), v2.toDate()); break; case QVariant::DateTime: *d->value.variant = qMax(v1.toDate(), v2.toDate()); break; case QVariant::Double: *d->value.variant = v1.toDouble() + v2.toDouble(); break; case QVariant::Int: *d->value.variant = v1.toInt() + v2.toInt(); break; case QVariant::List: *d->value.variant = v1.toList() + v2.toList(); break; case QVariant::LongLong: *d->value.variant = v1.toLongLong() + v2.toLongLong(); break; case QVariant::Point: *d->value.variant = v1.toPoint() + v2.toPoint(); break; case QVariant::PointF: *d->value.variant = v1.toPointF() + v2.toPointF(); break; case QVariant::String: *d->value.variant = QVariant(v1.toString() + v2.toString()); break; case QVariant::StringList: *d->value.variant = v1.toStringList() + v2.toStringList(); break; case QVariant::Time: { QTime t1 = v1.toTime(); QTime t2 = v2.toTime(); int h = t1.hour() + t2.hour(); int m = t1.minute() + t2.minute(); int s = t1.second() + t2.second(); int ms = t1.msec() + t2.msec(); if (ms > 999) { ms -= 999; s++; } if (s > 60) { s -= 60; m++; } if (m > 60) { m -= 60; h++; } if (h > 24) { h -= 24; } *d->value.variant = QTime(h, m, s, ms); } break; case QVariant::UInt: *d->value.variant = v1.toUInt() + v2.toUInt(); break; case QVariant::ULongLong: *d->value.variant = v1.toULongLong() + v2.toULongLong(); break; } } break; case Value::OrderedArray: case Value::UnorderedArray: case Value::AlternativeArray: { if (v.isArray()) { *(d->value.array) += *(v.d->value.array); } else { d->value.array->append(v); } } break; case Value::LangArray: { Q_ASSERT(v.type() == Value::LangArray); } break; case Value::Structure: { Q_ASSERT(v.type() == Value::Structure); break; } case Value::Rational: { Q_ASSERT(v.type() == Value::Rational); d->value.rational->numerator = (d->value.rational->numerator * v.d->value.rational->denominator) + (v.d->value.rational->numerator * d->value.rational->denominator); d->value.rational->denominator *= v.d->value.rational->denominator; break; } } return *this; } QMap Value::asLangArray() const { Q_ASSERT(d->type == LangArray); QMap langArray; Q_FOREACH (const KisMetaData::Value& val, *d->value.array) { - Q_ASSERT(val.d->propertyQualifiers.contains("xml:lang")); // TODO propably worth to have an assert for this in the constructor as well + Q_ASSERT(val.d->propertyQualifiers.contains("xml:lang")); // TODO probably worth to have an assert for this in the constructor as well KisMetaData::Value valKeyVal = val.d->propertyQualifiers.value("xml:lang"); Q_ASSERT(valKeyVal.type() == Variant); QVariant valKeyVar = valKeyVal.asVariant(); Q_ASSERT(valKeyVar.type() == QVariant::String); langArray[valKeyVar.toString()] = val; } return langArray; } QString Value::toString() const { switch (type()) { case Value::Invalid: return i18n("Invalid value."); case Value::Variant: return d->value.variant->toString(); break; case Value::OrderedArray: case Value::UnorderedArray: case Value::AlternativeArray: case Value::LangArray: { QString r = QString("[%1]{ ").arg(d->value.array->size()); for (int i = 0; i < d->value.array->size(); ++i) { const Value& val = d->value.array->at(i); r += val.toString(); if (i != d->value.array->size() - 1) { r += ','; } r += ' '; } r += '}'; return r; } case Value::Structure: { QString r = "{ "; QList fields = d->value.structure->keys(); for (int i = 0; i < fields.count(); ++i) { const QString& field = fields[i]; const Value& val = d->value.structure->value(field); r += field + " => " + val.toString(); if (i != d->value.array->size() - 1) { r += ','; } r += ' '; } r += '}'; return r; } break; case Value::Rational: return QString("%1 / %2").arg(d->value.rational->numerator).arg(d->value.rational->denominator); } return i18n("Invalid value."); } diff --git a/libs/image/tiles3/kis_memento_manager.cc b/libs/image/tiles3/kis_memento_manager.cc index 82564de2b2..2805cfdbbd 100644 --- a/libs/image/tiles3/kis_memento_manager.cc +++ b/libs/image/tiles3/kis_memento_manager.cc @@ -1,425 +1,425 @@ /* * Copyright (c) 2009 Dmitry Kazakov * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include "kis_memento_manager.h" #include "kis_memento.h" //#define DEBUG_MM #ifdef DEBUG_MM #define DEBUG_LOG_TILE_ACTION(action, tile, col, row) \ printf("### MementoManager (0x%X): %s " \ "\ttile:\t0x%X (%d, %d) ###\n", (quintptr)this, action, \ (quintptr)tile, col, row) #define DEBUG_LOG_SIMPLE_ACTION(action) \ printf("### MementoManager (0x%X): %s\n", (quintptr)this, action) #define DEBUG_DUMP_MESSAGE(action) do { \ printf("\n### MementoManager (0x%X): %s \t\t##########\n", \ (quintptr)this, action); \ debugPrintInfo(); \ printf("##################################################################\n\n"); \ } while(0) #else #define DEBUG_LOG_TILE_ACTION(action, tile, col, row) #define DEBUG_LOG_SIMPLE_ACTION(action) #define DEBUG_DUMP_MESSAGE(action) #endif /** * The class is supposed to store the changes of the paint device * it is associated with. The history of changes is presented in form * of transactions (revisions). If you purge the history of one * transaction (revision) with purgeHistory() we won't be able to undo * the changes made by this transactions. * * The Memento Manager can be in two states: * - Named Transaction is in progress - it means the caller * has explicitly requested creation of a new transaction. * The handle for the transaction is stored on a side of * the caller. And the history will be automatically purged * when the handler dies. * - Anonymous Transaction is in progress - the caller isn't * bothered about transactions at all. We pretend as we do * not support any versioning and do not have any historical * information. The history of such transactions is not purged * automatically, but it is free'd when younger named transaction * is purged. */ #define blockRegistration() (m_registrationBlocked = true) #define unblockRegistration() (m_registrationBlocked = false) #define registrationBlocked() (m_registrationBlocked) #define namedTransactionInProgress() ((bool)m_currentMemento) KisMementoManager::KisMementoManager() : m_index(0), m_headsHashTable(0), m_registrationBlocked(false) { /** * Tile change/delete registration is enabled for all * devices by default. It can't be delayed. */ } KisMementoManager::KisMementoManager(const KisMementoManager& rhs) : m_index(rhs.m_index, 0), m_revisions(rhs.m_revisions), m_cancelledRevisions(rhs.m_cancelledRevisions), m_headsHashTable(rhs.m_headsHashTable, 0), m_currentMemento(rhs.m_currentMemento), m_registrationBlocked(rhs.m_registrationBlocked) { Q_ASSERT_X(!m_registrationBlocked, "KisMementoManager", "(impossible happened) " "The device has been copied while registration was blocked"); } KisMementoManager::~KisMementoManager() { // Nothing to be done here. Happily... // Everything is done by QList and KisSharedPtr... DEBUG_LOG_SIMPLE_ACTION("died\n"); } /** * NOTE: We don't assume that the registerTileChange/Delete * can be called once a commit only. Reverse can happen when we * do sequential clears of the device. In such a case the tiles * will be removed and added several times during a commit. * * TODO: There is an 'uncomfortable' state for the tile possible * 1) Imagine we have a clear device * 2) Then we painted something in a tile * 3) It registered itself using registerTileChange() * 4) Then we called clear() and getMemento() [==commit()] * 5) The tile will be registered as deleted and successfully * committed to a revision. That means the states of the memento - * manager at stages 1 and 5 do not coinside. + * manager at stages 1 and 5 do not coincide. * This will not lead to any memory leaks or bugs seen, it just * not good from a theoretical perspective. */ void KisMementoManager::registerTileChange(KisTile *tile) { if (registrationBlocked()) return; DEBUG_LOG_TILE_ACTION("reg. [C]", tile, tile->col(), tile->row()); KisMementoItemSP mi = m_index.getExistingTile(tile->col(), tile->row()); if(!mi) { mi = new KisMementoItem(); mi->changeTile(tile); m_index.addTile(mi); if(namedTransactionInProgress()) m_currentMemento->updateExtent(mi->col(), mi->row()); } else { mi->reset(); mi->changeTile(tile); } } void KisMementoManager::registerTileDeleted(KisTile *tile) { if (registrationBlocked()) return; DEBUG_LOG_TILE_ACTION("reg. [D]", tile, tile->col(), tile->row()); KisMementoItemSP mi = m_index.getExistingTile(tile->col(), tile->row()); if(!mi) { mi = new KisMementoItem(); mi->deleteTile(tile, m_headsHashTable.defaultTileData()); m_index.addTile(mi); if(namedTransactionInProgress()) m_currentMemento->updateExtent(mi->col(), mi->row()); } else { mi->reset(); mi->deleteTile(tile, m_headsHashTable.defaultTileData()); } } void KisMementoManager::commit() { if (m_index.isEmpty()) { if(namedTransactionInProgress()) { //warnTiles << "Named Transaction is empty"; /** * We still need to continue commit, because * a named transaction may be reverted by the user */ } else { m_currentMemento = 0; return; } } KisMementoItemList revisionList; KisMementoItemSP mi; KisMementoItemSP parentMI; bool newTile; KisMementoItemHashTableIterator iter(&m_index); while ((mi = iter.tile())) { parentMI = m_headsHashTable.getTileLazy(mi->col(), mi->row(), newTile); mi->setParent(parentMI); mi->commit(); revisionList.append(mi); m_headsHashTable.deleteTile(mi->col(), mi->row()); iter.moveCurrentToHashTable(&m_headsHashTable); //iter.next(); // previous line does this for us } KisHistoryItem hItem; hItem.itemList = revisionList; hItem.memento = m_currentMemento.data(); m_revisions.append(hItem); m_currentMemento = 0; Q_ASSERT(m_index.isEmpty()); DEBUG_DUMP_MESSAGE("COMMIT_DONE"); // Waking up pooler to prepare copies for us KisTileDataStore::instance()->kickPooler(); } KisTileSP KisMementoManager::getCommitedTile(qint32 col, qint32 row, bool &existingTile) { /** * Our getOldTile mechanism is supposed to return current * tile, if the history is disabled. So we return zero if * no named transaction is in progress. */ if(!namedTransactionInProgress()) return KisTileSP(); KisMementoItemSP mi = m_headsHashTable.getReadOnlyTileLazy(col, row, existingTile); Q_ASSERT(mi); return mi->tile(0); } KisMementoSP KisMementoManager::getMemento() { /** * We do not allow nested transactions */ Q_ASSERT(!namedTransactionInProgress()); // Clear redo() information m_cancelledRevisions.clear(); commit(); m_currentMemento = new KisMemento(this); DEBUG_LOG_SIMPLE_ACTION("GET_MEMENTO_DONE"); return m_currentMemento; } KisMementoSP KisMementoManager::currentMemento() { return m_currentMemento; } #define forEachReversed(iter, list) \ for(iter=list.end(); iter-- != list.begin();) void KisMementoManager::rollback(KisTileHashTable *ht) { commit(); if (! m_revisions.size()) return; KisHistoryItem changeList = m_revisions.takeLast(); KisMementoItemSP mi; KisMementoItemSP parentMI; KisMementoItemList::iterator iter; blockRegistration(); forEachReversed(iter, changeList.itemList) { mi=*iter; parentMI = mi->parent(); if (mi->type() == KisMementoItem::CHANGED) ht->deleteTile(mi->col(), mi->row()); if (parentMI->type() == KisMementoItem::CHANGED) ht->addTile(parentMI->tile(this)); m_headsHashTable.deleteTile(parentMI->col(), parentMI->row()); m_headsHashTable.addTile(parentMI); // This is not necessary //mi->setParent(0); } /** * NOTE: tricky hack alert. * We have just deleted some tiles from the original hash table. * And they accurately reported to us about their death. Should * have reported... But we have prevented their registration with * explicitly blocking the process. So all the dead tiles are * going to /dev/null :) * * PS: It could cause some race condition... But we insist on * serialization of rollback()/rollforward() requests. There is * not much sense in calling rollback() concurrently. */ unblockRegistration(); // We have just emulated a commit so: m_currentMemento = 0; Q_ASSERT(!namedTransactionInProgress()); m_cancelledRevisions.prepend(changeList); DEBUG_DUMP_MESSAGE("UNDONE"); // Waking up pooler to prepare copies for us KisTileDataStore::instance()->kickPooler(); } void KisMementoManager::rollforward(KisTileHashTable *ht) { Q_ASSERT(m_index.isEmpty()); if (!m_cancelledRevisions.size()) return; KisHistoryItem changeList = m_cancelledRevisions.takeFirst(); KisMementoItemSP mi; blockRegistration(); Q_FOREACH (mi, changeList.itemList) { if (mi->parent()->type() == KisMementoItem::CHANGED) ht->deleteTile(mi->col(), mi->row()); if (mi->type() == KisMementoItem::CHANGED) ht->addTile(mi->tile(this)); m_index.addTile(mi); } // see comment in rollback() m_currentMemento = changeList.memento; commit(); unblockRegistration(); DEBUG_DUMP_MESSAGE("REDONE"); } void KisMementoManager::purgeHistory(KisMementoSP oldestMemento) { if (m_currentMemento == oldestMemento) { commit(); } qint32 revisionIndex = findRevisionByMemento(oldestMemento); if (revisionIndex < 0) return; for(; revisionIndex > 0; revisionIndex--) { resetRevisionHistory(m_revisions.first().itemList); m_revisions.removeFirst(); } Q_ASSERT(m_revisions.first().memento == oldestMemento); resetRevisionHistory(m_revisions.first().itemList); DEBUG_DUMP_MESSAGE("PURGE_HISTORY"); } qint32 KisMementoManager::findRevisionByMemento(KisMementoSP memento) const { qint32 index = -1; for(qint32 i = 0; i < m_revisions.size(); i++) { if (m_revisions[i].memento == memento) { index = i; break; } } return index; } void KisMementoManager::resetRevisionHistory(KisMementoItemList list) { KisMementoItemSP parentMI; KisMementoItemSP mi; Q_FOREACH (mi, list) { parentMI = mi->parent(); if(!parentMI) continue; while (parentMI->parent()) { parentMI = parentMI->parent(); } mi->setParent(parentMI); } } void KisMementoManager::setDefaultTileData(KisTileData *defaultTileData) { m_headsHashTable.setDefaultTileData(defaultTileData); m_index.setDefaultTileData(defaultTileData); } void KisMementoManager::debugPrintInfo() { printf("KisMementoManager stats:\n"); printf("Index list\n"); KisMementoItemSP mi; KisMementoItemHashTableIteratorConst iter(&m_index); while ((mi = iter.tile())) { mi->debugPrintInfo(); iter.next(); } printf("Revisions list:\n"); qint32 i = 0; Q_FOREACH (const KisHistoryItem &changeList, m_revisions) { printf("--- revision #%d ---\n", i++); Q_FOREACH (mi, changeList.itemList) { mi->debugPrintInfo(); } } printf("\nCancelled revisions list:\n"); i = 0; Q_FOREACH (const KisHistoryItem &changeList, m_cancelledRevisions) { printf("--- revision #%d ---\n", m_revisions.size() + i++); Q_FOREACH (mi, changeList.itemList) { mi->debugPrintInfo(); } } printf("----------------\n"); m_headsHashTable.debugPrintInfo(); } diff --git a/libs/libqml/plugins/components/PageStack.qml b/libs/libqml/plugins/components/PageStack.qml index 23c697a621..00e2d1744e 100644 --- a/libs/libqml/plugins/components/PageStack.qml +++ b/libs/libqml/plugins/components/PageStack.qml @@ -1,546 +1,546 @@ /**************************************************************************** ** ** Copyright (C) 2011 Marco Martin ** ** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). ** All rights reserved. ** Contact: Nokia Corporation (qt-info@nokia.com) ** ** This file is part of the Qt Components project. ** ** $QT_BEGIN_LICENSE:BSD$ ** You may use this file under the terms of the BSD license as follows: ** ** "Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are ** met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in ** the documentation and/or other materials provided with the ** distribution. ** * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor ** the names of its contributors may be used to endorse or promote ** products derived from this software without specific prior written ** permission. ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ** $QT_END_LICENSE$ ** ****************************************************************************/ // The PageStack item defines a container for pages and a stack-based // navigation model. Pages can be defined as QML items or components. /**Documented API Inherits: Item Imports: QtQuick 1.1 . PageStack.js Description: The PageStack component provides a stack-based navigation model that you can use in your application. A stack-based navigation model means that a page of content for your application is pushed onto a stack when the user navigates deeper into the application page hierarchy. The user can then go back to the previous page (or several pages back) by popping a page (or several pages) off the top of the stack. Properties: * int depth: The number of pages on the page stack. * Item currentPage: The page in the stack that is currently visible. * ToolBar toolBar: The toolbar container for the tools associated with each page. If a toolbar is specified, the tools set for the current page is shown to the user. If toolbar is null, then no tools are shown even if a page does have tools. * variant initialPage: The page to be automatically loaded when this PageStack component gets instantiated. * bool busy: Indicates whether there is an ongoing page transition. Methods: * clear(): Clears the page stack of all pages. * find(function): This iterates, top to bottom, through all the pages in the page stack and passes each page to the given function. If the specified function returns true, the iterating stops and this function returns the page that produced the true result. If no matching page is found in the page stack, null is returned. * pop(page, immediate): When you use pop() with no arguments, it pops the top page off the stack and returns that page to the caller. The normal pop transition animation is performed. If the page popped off the stack is based on the Item element, the page is re-parented back to its original parent. If the page didn't have an original parent (ie, was created with push(Qt.createComponent("foo.qml")) the page instance will be deleted. If you give a page argument, the stack is unwound to the given page. Any Item-based pages popped off the stack are re-parented to their original parent. If the given page is not found in the stack, the stack is unwound to the first page in the stack. However, if you specifically want to unwind the page stack to the first page in the stack, it is best to be explicit about what you are doing and use pop(null) rather than guessing a page that is not on the stack. The immediate argument defaults to false which means the normal transition animation is performed when a page is popped. If you do not want the transition animation to be performed pass a value of true for immediate. Note: A pop() on a stack with that contains 0 or 1 pages is a no-operation. Returns: The page that was top-most on the stack before the pop operation. * push(page, properties, immediate): Pushes the given page onto the page stack. You can use a component, item or string for the page. If the page is based on the Item element, the page is re-parented. If a string is used then it is interpreted as a URL that is used to load a page component. The push operation results in the appropriate transition animation being run. If you are pushing an array of pages, the transition animation is only shown for the last page. Returns: The new top page on the stack. The page argument can also be an array of pages. In this case, all the pages in the array are pushed onto the stack. The items in the array can be components, items or strings just like for pushing a single page. The page argument can also be an object that specifies a page with properties or even an array of pages with properties. The properties argument is optional and allows you to specify values for properties in the page being pushed. The immediate argument defaults to false which means the normal transition animation is performed when a page is pushed. If you do not want the transition animation to be performed pass a value of true for immediate. Note: When the stack is empty, a push() or replace() does not perform a transition animation because there is no page to transition from. The only time this normally happens is when an application is starting up so it is not appropriate to have a transition animation anyway. See also Page. * replace(page, properties, immediate): Replaces the top-most page on the stack with page. As in the push() operation, you can use a component, item or string for the page, or even an array of pages. If the page is based on the Item element, the page is re- parented. As in the pop() operation, the replaced page on the stack is re- parented back to its original parent. Returns: The new top page on the stack. See also push(). **/ import QtQuick 2.3 import org.krita.sketch 1.0 import "." import "PageStack.js" as Engine Item { id: root width: parent ? parent.width : 0 height: parent ? parent.height : 0 property int depth: Engine.getDepth() property Item currentPage: null property Item toolBar property variant initialPage property int transitionDuration: Constants.AnimationDuration // Indicates whether there is an ongoing page transition. property bool busy: internal.ongoingTransitionCount > 0 // Pushes a page on the stack. // The page can be defined as a component, item or string. // If an item is used then the page will get re-parented. // If a string is used then it is interpreted as a url that is used to load a page component. // // The page can also be given as an array of pages. In this case all those pages will be pushed // onto the stack. The items in the stack can be components, items or strings just like for single // pages. Additionally an object can be used, which specifies a page and an optional properties // property. This can be used to push multiple pages while still giving each of them properties. // When an array is used the transition animation will only be to the last page. // // The properties argument is optional and allows defining a map of properties to set on the page. // If the immediate argument is true then no transition animation is performed. // Returns the page instance. function push(page, properties, immediate) { return Engine.push(page, properties, false, immediate); } // Pops a page off the stack. // If page is specified then the stack is unwound to that page, to unwind to the first page specify // page as null. If the immediate argument is true then no transition animation is performed. // Returns the page instance that was popped off the stack. function pop(page, immediate) { return Engine.pop(page, immediate); } // Replaces a page on the stack. // See push() for details. function replace(page, properties, immediate) { return Engine.push(page, properties, true, immediate); } // Clears the page stack. function clear() { return Engine.clear(); } // Iterates through all pages (top to bottom) and invokes the specified function. // If the specified function returns true the search stops and the find function // returns the page that the iteration stopped at. If the search doesn't result // in any page being found then null is returned. function find(func) { return Engine.find(func); } // Called when the page stack visibility changes. onVisibleChanged: { if (currentPage) { internal.setPageStatus(currentPage, visible ? pageStatus.active : pageStatus.inactive); if (visible) currentPage.visible = currentPage.parent.visible = true; } } onInitialPageChanged: { if (!internal.completed) { return } if (initialPage) { if (depth == 0) { push(initialPage, null, true) } else if (depth == 1) { replace(initialPage, null, true) } else { console.log("Cannot update PageStack.initialPage") } } } Component.onCompleted: { internal.completed = true if (initialPage && depth == 0) push(initialPage, null, true) } QtObject { id: internal // The number of ongoing transitions. property int ongoingTransitionCount: 0 //FIXME: there should be a way to access to theh without storing it in an ugly way property bool completed: false // Sets the page status. function setPageStatus(page, status) { if (page != null) { if (page.status !== undefined) { if (status == pageStatus.active && page.status == pageStatus.inactive) page.status = pageStatus.activating; else if (status == pageStatus.inactive && page.status == pageStatus.active) page.status = pageStatus.deactivating; page.status = status; } } } } QtObject { id: pageStatus; property int active: 0; property int inactive: 1; property int activating: 2; property int deactivating: 3; } // Component for page containers. Component { id: containerComponent Item { id: container width: parent ? parent.width : 0 height: parent ? parent.height : 0 // The states correspond to the different possible positions of the container. state: "Hidden" // The page held by this container. property Item page: null // The owner of the page. property Item owner: null // The width of the longer stack dimension property int stackWidth: Math.max(root.width, root.height) // Duration of transition animation (in ms) // Flag that indicates the container should be cleaned up after the transition has ended. property bool cleanupAfterTransition: false // Flag that indicates if page transition animation is running property bool transitionAnimationRunning: false // State to be set after previous state change animation has finished property string pendingState: "none" // Ensures that transition finish actions are executed // in case the object is destroyed before reaching the // end state of an ongoing transition Component.onDestruction: { if (transitionAnimationRunning) transitionEnded(); } // Sets pending state as current if state change is delayed onTransitionAnimationRunningChanged: { if (!transitionAnimationRunning && pendingState != "none") { state = pendingState; pendingState = "none"; } } - // Handles state change depening on transition animation status + // Handles state change depending on transition animation status function setState(newState) { if (transitionAnimationRunning) pendingState = newState; else state = newState; } // Performs a push enter transition. function pushEnter(immediate, orientationChanges) { if (!immediate) { if (orientationChanges) setState("LandscapeRight"); else setState("Right"); } setState(""); page.visible = true; if (root.visible && immediate) internal.setPageStatus(page, pageStatus.active); } // Performs a push exit transition. function pushExit(replace, immediate, orientationChanges) { if (orientationChanges) setState(immediate ? "Hidden" : "LandscapeLeft"); else setState(immediate ? "Hidden" : "Left"); if (root.visible && immediate) internal.setPageStatus(page, pageStatus.inactive); if (replace) { if (immediate) cleanup(); else cleanupAfterTransition = true; } } // Performs a pop enter transition. function popEnter(immediate, orientationChanges) { if (!immediate) state = orientationChanges ? "LandscapeLeft" : "Left"; setState(""); page.visible = true; if (root.visible && immediate) internal.setPageStatus(page, pageStatus.active); } // Performs a pop exit transition. function popExit(immediate, orientationChanges) { if (orientationChanges) setState(immediate ? "Hidden" : "LandscapeRight"); else setState(immediate ? "Hidden" : "Right"); if (root.visible && immediate) internal.setPageStatus(page, pageStatus.inactive); if (immediate) cleanup(); else cleanupAfterTransition = true; } // Called when a transition has started. function transitionStarted() { transitionAnimationRunning = true; internal.ongoingTransitionCount++; if (root.visible) { internal.setPageStatus(page, (state == "") ? pageStatus.activating : pageStatus.deactivating); } } // Called when a transition has ended. function transitionEnded() { if (state != "") state = "Hidden"; if (root.visible) internal.setPageStatus(page, (state == "") ? pageStatus.active : pageStatus.inactive); internal.ongoingTransitionCount--; transitionAnimationRunning = false; if (cleanupAfterTransition) cleanup(); } states: [ // Explicit properties for default state. State { name: "" PropertyChanges { target: container; visible: true; opacity: 1 } }, // Start state for pop entry, end state for push exit. State { name: "Left" PropertyChanges { target: container; x: -width / 2; opacity: 0 } }, // Start state for pop entry, end state for push exit // when exiting portrait and entering landscape. State { name: "LandscapeLeft" PropertyChanges { target: container; x: -stackWidth / 2; opacity: 0 } }, // Start state for push entry, end state for pop exit. State { name: "Right" PropertyChanges { target: container; x: width / 2; opacity: 0 } }, // Start state for push entry, end state for pop exit // when exiting portrait and entering landscape. State { name: "LandscapeRight" PropertyChanges { target: container; x: stackWidth / 2; opacity: 0 } }, // Inactive state. State { name: "Hidden" PropertyChanges { target: container; visible: false } } ] transitions: [ // Push exit transition Transition { from: ""; to: "Left" SequentialAnimation { ScriptAction { script: transitionStarted() } ParallelAnimation { PropertyAnimation { properties: "x"; easing.type: Easing.InQuad; duration: transitionDuration } PropertyAnimation { properties: "opacity"; easing.type: Easing.Linear; duration: transitionDuration } } ScriptAction { script: transitionEnded() } } }, // Pop entry transition Transition { from: "Left"; to: "" SequentialAnimation { ScriptAction { script: transitionStarted() } ParallelAnimation { PropertyAnimation { properties: "x"; easing.type: Easing.OutQuad; duration: transitionDuration } PropertyAnimation { properties: "opacity"; easing.type: Easing.Linear; duration: transitionDuration } } ScriptAction { script: transitionEnded() } } }, // Push exit transition landscape Transition { from: ""; to: "LandscapeLeft" SequentialAnimation { ScriptAction { script: transitionStarted() } ParallelAnimation { PropertyAnimation { properties: "x"; easing.type: Easing.InQuad; duration: transitionDuration } PropertyAnimation { properties: "opacity"; easing.type: Easing.Linear; duration: transitionDuration } } ScriptAction { script: transitionEnded() } } }, // Pop entry transition landscape Transition { from: "LandscapeLeft"; to: "" SequentialAnimation { ScriptAction { script: transitionStarted() } ParallelAnimation { PropertyAnimation { properties: "x"; easing.type: Easing.OutQuad; duration: transitionDuration } PropertyAnimation { properties: "opacity"; easing.type: Easing.Linear; duration: transitionDuration } } ScriptAction { script: transitionEnded() } } }, // Pop exit transition Transition { from: ""; to: "Right" SequentialAnimation { ScriptAction { script: transitionStarted() } ParallelAnimation { PropertyAnimation { properties: "x"; easing.type: Easing.InQuad; duration: transitionDuration } PropertyAnimation { properties: "opacity"; easing.type: Easing.Linear; duration: transitionDuration } } // Workaround for transition animation bug causing ghost view with page pop transition animation // TODO: Root cause still unknown PropertyAnimation {} ScriptAction { script: transitionEnded() } } }, // Push entry transition Transition { from: "Right"; to: "" SequentialAnimation { ScriptAction { script: transitionStarted() } ParallelAnimation { PropertyAnimation { properties: "x"; easing.type: Easing.OutQuad; duration: transitionDuration } PropertyAnimation { properties: "opacity"; easing.type: Easing.Linear; duration: transitionDuration } } ScriptAction { script: transitionEnded() } } }, // Pop exit transition landscape Transition { from: ""; to: "LandscapeRight" SequentialAnimation { ScriptAction { script: transitionStarted() } ParallelAnimation { PropertyAnimation { properties: "x"; easing.type: Easing.InQuad; duration: transitionDuration } PropertyAnimation { properties: "opacity"; easing.type: Easing.Linear; duration: transitionDuration } } // Workaround for transition animation bug causing ghost view with page pop transition animation // TODO: Root cause still unknown PropertyAnimation {} ScriptAction { script: transitionEnded() } } }, // Push entry transition landscape Transition { from: "LandscapeRight"; to: "" SequentialAnimation { ScriptAction { script: transitionStarted() } ParallelAnimation { PropertyAnimation { properties: "x"; easing.type: Easing.OutQuad; duration: transitionDuration } PropertyAnimation { properties: "opacity"; easing.type: Easing.Linear; duration: transitionDuration } } ScriptAction { script: transitionEnded() } } } ] // Cleans up the container and then destroys it. function cleanup() { if (page != null) { if (page.status == pageStatus.active) { internal.setPageStatus(page, pageStatus.inactive) } if (owner != container) { // container is not the owner of the page - re-parent back to original owner page.visible = false; page.anchors.fill = undefined page.parent = owner; } } container.destroy(); } } } } diff --git a/libs/pigment/KoColorProfileStorage.h b/libs/pigment/KoColorProfileStorage.h index d1e71fe78e..45da5e29fc 100644 --- a/libs/pigment/KoColorProfileStorage.h +++ b/libs/pigment/KoColorProfileStorage.h @@ -1,120 +1,120 @@ /* * Copyright (c) 2017 Dmitry Kazakov * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KOCOLORPROFILESTORAGE_H #define KOCOLORPROFILESTORAGE_H #include class QByteArray; class QString; class KoColorProfile; class KoColorSpaceFactory; /** * @brief The KoColorProfileStorage class is a "composite subclass" of * KoColorSpaceRegistry that ensures that the access to profiles is guarded * by a separate lock and the hierarchy of locks is always followed (which * avoid deadlocks). * * Registry locking hierarchy is basically the following: * * 1) KoColorSpaceRegistry::Private::registrylock * 2) KoColorProfileStorage::Private::lock * * It means that we can take any single lock if we need it separately, but * if we need to take both of them, we should always take them is a specified * order. * * Encapsulation of the profile accesses inside a separate class lets us * follow this rule without even thinking of it. KoColorProfileStorage just - * *never* calls any method of the registry, therefore lock order inverion is + * *never* calls any method of the registry, therefore lock order inversion is * not possible, */ class KoColorProfileStorage { public: KoColorProfileStorage(); ~KoColorProfileStorage(); /** * Register the profile in the storage * @param profile the new profile to be registered */ void addProfile(KoColorProfile* profile); void addProfile(const KoColorProfile* profile); // TODO /** * Removes the profile from the storage. * Please note that the caller should delete the profile object himself! * * @param profile the profile to be removed */ void removeProfile(KoColorProfile* profile); /** * @brief containsProfile shows if a profile is registered in the storage */ bool containsProfile(const KoColorProfile *profile); /** * Create an alias to a profile with a different name. Then @ref profileByName * will return the profile @p to when passed @p name as a parameter. */ void addProfileAlias(const QString& name, const QString& to); /** * @return the profile alias, or name if not aliased */ QString profileAlias(const QString& name) const; /** * Return a profile by its given name, or 0 if none registered. * @return a profile by its given name, or 0 if none registered. * @param name the product name as set on the profile. * @see addProfile() * @see KoColorProfile::productName() */ const KoColorProfile * profileByName(const QString & name) const ; /** * Returns a profile by its unique id stored/calculated in the header. * The first call to this function might take long, because the map is * created on the first use only (atm used by SVG only) * @param id unique ProfileID of the profile (MD5 sum of its header) * @return the profile or 0 if not found */ const KoColorProfile *profileByUniqueId(const QByteArray &id) const; /** * Return the list of profiles for a colorspace represented by its factory. * Profiles will not work with any color space, you can query which profiles * that are registered with this registry can be used in combination with the * argument factory. * @param csf is a factory for the requested color space * @return a list of profiles for the factory */ QList profilesFor(const KoColorSpaceFactory * csf) const; private: struct Private; const QScopedPointer d; }; #endif // KOCOLORPROFILESTORAGE_H diff --git a/libs/pigment/tests/CCSGraph.cpp b/libs/pigment/tests/CCSGraph.cpp index efdb0e2c8e..55f43011c2 100644 --- a/libs/pigment/tests/CCSGraph.cpp +++ b/libs/pigment/tests/CCSGraph.cpp @@ -1,119 +1,119 @@ /* * Copyright (c) 2007-2008 Cyrille Berger * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include #include #include #include #include #include "KoColorSpaceRegistry.h" #include "KoColorConversionSystem.h" #include #include #include struct FriendOfColorSpaceRegistry { static QString toDot() { return KoColorSpaceRegistry::instance()->colorConversionSystem()->toDot(); } static QString bestPathToDot(const QString &srcKey, const QString &dstKey) { return KoColorSpaceRegistry::instance()->colorConversionSystem()->bestPathToDot(srcKey, dstKey); } }; int main(int argc, char** argv) { QCoreApplication app(argc, argv); QCommandLineParser parser; parser.addVersionOption(); parser.addHelpOption(); // Initialize the list of options parser.addOption(QCommandLineOption(QStringList() << QLatin1String("graphs"), i18n("return the list of available graphs"))); parser.addOption(QCommandLineOption(QStringList() << QLatin1String("graph"), i18n("specify the type of graph (see --graphs to get the full list, the default is full)"), QLatin1String("type"), QLatin1String("full"))); parser.addOption(QCommandLineOption(QStringList() << QLatin1String("key"), i18n("specify the key of the source color space"), QLatin1String("key"), QString())); parser.addOption(QCommandLineOption(QStringList() << QLatin1String("key"), i18n("specify the key of the destination color space"), QLatin1String("key"), QString())); parser.addOption(QCommandLineOption(QStringList() << QLatin1String("output"), i18n("specify the output (can be ps or dot, the default is ps)"), QLatin1String("type"), QLatin1String("ps"))); parser.addPositionalArgument(QLatin1String("outputfile"), i18n("name of the output file")); parser.process(app); // PORTING SCRIPT: move this to after any parser.addOption if (parser.isSet("graphs")) { - // Don't change those lines to use dbgPigment derivatives, they need to be outputed + // Don't change those lines to use dbgPigment derivatives, they need to be outputted // to stdout not stderr. std::cout << "full : show all the connection on the graph" << std::endl; std::cout << "bestpath : show the best path for a given transformation" << std::endl; exit(EXIT_SUCCESS); } QString graphType = parser.value("graph"); QString outputType = parser.value("output"); if (parser.positionalArguments().count() != 1) { errorPigment << "No output file name specified"; parser.showHelp(); exit(EXIT_FAILURE); } QString outputFileName = parser.positionalArguments()[0]; // Generate the graph QString dot; if (graphType == "full") { dot = FriendOfColorSpaceRegistry::toDot(); } else if (graphType == "bestpath") { QString srcKey = parser.value("src-key"); QString dstKey = parser.value("dst-key"); if (srcKey.isEmpty() || dstKey.isEmpty()) { errorPigment << "src-key and dst-key must be specified for the graph bestpath"; exit(EXIT_FAILURE); } else { dot = FriendOfColorSpaceRegistry::bestPathToDot(srcKey, dstKey); } } else { errorPigment << "Unknown graph type : " << graphType.toLatin1(); exit(EXIT_FAILURE); } if (outputType == "dot") { QFile file(outputFileName); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) exit(EXIT_FAILURE); QTextStream out(&file); out << dot; } else if (outputType == "ps" || outputType == "svg") { QTemporaryFile file; if (!file.open()) { exit(EXIT_FAILURE); } QTextStream out(&file); out << dot; QString cmd = QString("dot -T%1 %2 -o %3").arg(outputType).arg(file.fileName()).arg(outputFileName); file.close(); if (QProcess::execute(cmd) != 0) { errorPigment << "An error has occurred when executing : '" << cmd << "' the most likely cause is that 'dot' command is missing, and that you should install graphviz (from http://www.graphiz.org)"; } } else { errorPigment << "Unknown output type : " << outputType; exit(EXIT_FAILURE); } } diff --git a/libs/ui/KisImportExportFilter.h b/libs/ui/KisImportExportFilter.h index 879a7c194f..8fa8b4d961 100644 --- a/libs/ui/KisImportExportFilter.h +++ b/libs/ui/KisImportExportFilter.h @@ -1,183 +1,183 @@ /* This file is part of the Calligra libraries Copyright (C) 2001 Werner Trobin 2002 Werner Trobin This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_IMPORT_EXPORT_FILTER_H #define KIS_IMPORT_EXPORT_FILTER_H #include #include #include #include #include #include #include #include #include #include #include #include class KoUpdater; class KisDocument; class KisConfigWidget; #include "kritaui_export.h" /** * @brief The base class for import and export filters. * * Derive your filter class from this base class and implement * the @ref convert() method. Don't forget to specify the Q_OBJECT * macro in your class even if you don't use signals or slots. * This is needed as filters are created on the fly. * * @note Take care: The m_chain pointer is invalid while the constructor * runs due to the implementation -- @em don't use it in the constructor. * After the constructor, when running the @ref convert() method it's * guaranteed to be valid, so no need to check against 0. * * @note If the code is compiled in debug mode, setting CALLIGRA_DEBUG_FILTERS * environment variable to any value disables deletion of temporary files while * importing/exporting. This is useful for testing purposes. * * @author Werner Trobin * @todo the class has no constructor and therefore cannot initialize its private class */ class KRITAUI_EXPORT KisImportExportFilter : public QObject { Q_OBJECT public: static const QString ImageContainsTransparencyTag; static const QString ColorModelIDTag; static const QString ColorDepthIDTag; static const QString sRGBTag; public: /** * This enum is used to signal the return state of your filter. * Return OK in @ref convert() in case everything worked as expected. * Feel free to add some more error conditions @em before the last item * if it's needed. */ enum ConversionStatus { OK, UsageError, CreationError, FileNotFound, StorageCreationError, BadMimeType, BadConversionGraph, WrongFormat, NotImplemented, ParsingError, InternalError, UserCancelled, InvalidFormat, FilterCreationError, ProgressCancelled, UnsupportedVersion, JustInCaseSomeBrokenCompilerUsesLessThanAByte = 255 }; ~KisImportExportFilter() override; void setBatchMode(bool batchmode); void setFilename(const QString &filename); void setRealFilename(const QString &filename); void setMimeType(const QString &mime); void setUpdater(QPointer updater); /** * The filter chain calls this method to perform the actual conversion. * The passed mimetypes should be a pair of those you specified in your * .desktop file. * You @em have to implement this method to make the filter work. * * @return The error status, see the @ref #ConversionStatus enum. * KisImportExportFilter::OK means that everything is alright. */ virtual ConversionStatus convert(KisDocument *document, QIODevice *io, KisPropertiesConfigurationSP configuration = 0) = 0; /** * Get the text version of the status value */ static QString conversionStatusString(ConversionStatus status); /** * @brief defaultConfiguration defines the default settings for the given import export filter * @param from The mimetype of the source file/document * @param to The mimetype of the destination file/document * @return a serializable KisPropertiesConfiguration object */ virtual KisPropertiesConfigurationSP defaultConfiguration(const QByteArray& from = "", const QByteArray& to = "") const; /** * @brief lastSavedConfiguration return the last saved configuration for this filter * @param from The mimetype of the source file/document * @param to The mimetype of the destination file/document * @return a serializable KisPropertiesConfiguration object */ KisPropertiesConfigurationSP lastSavedConfiguration(const QByteArray &from = "", const QByteArray &to = "") const; /** * @brief createConfigurationWidget creates a widget that can be used to define the settings for a given import/export filter - * @param parent the ownder of the widget; the caller is responsible for deleting + * @param parent the owner of the widget; the caller is responsible for deleting * @param from The mimetype of the source file/document * @param to The mimetype of the destination file/document * * @return the widget */ virtual KisConfigWidget *createConfigurationWidget(QWidget *parent, const QByteArray& from = "", const QByteArray& to = "") const; /** * @brief generate and return the list of capabilities of this export filter. The list * @return returns the list of capabilities of this export filter */ virtual QMap exportChecks(); /// Override and return false for the filters that use a library that cannot handle file handles, only file names. virtual bool supportsIO() const { return true; } protected: /** * This is the constructor your filter has to call, obviously. */ KisImportExportFilter(QObject *parent = 0); QString filename() const; QString realFilename() const; bool batchMode() const; QByteArray mimeType() const; void setProgress(int value); virtual void initializeCapabilities(); void addCapability(KisExportCheckBase *capability); void addSupportedColorModels(QList > supportedColorModels, const QString &name, KisExportCheckBase::Level level = KisExportCheckBase::PARTIALLY); private: KisImportExportFilter(const KisImportExportFilter& rhs); KisImportExportFilter& operator=(const KisImportExportFilter& rhs); class Private; Private *const d; }; #endif diff --git a/libs/ui/canvas/kis_canvas_widget_base.cpp b/libs/ui/canvas/kis_canvas_widget_base.cpp index c6a8541044..c16c6c0802 100644 --- a/libs/ui/canvas/kis_canvas_widget_base.cpp +++ b/libs/ui/canvas/kis_canvas_widget_base.cpp @@ -1,273 +1,273 @@ /* * Copyright (C) 2007, 2010 Adrian Page * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_canvas_widget_base.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_coordinates_converter.h" #include "kis_canvas_decoration.h" #include "kis_config.h" #include "kis_canvas2.h" #include "KisViewManager.h" #include "kis_selection_manager.h" #include "KisDocument.h" #include "kis_update_info.h" struct KisCanvasWidgetBase::Private { public: Private(KisCanvas2 *newCanvas, KisCoordinatesConverter *newCoordinatesConverter) : canvas(newCanvas) , coordinatesConverter(newCoordinatesConverter) , viewConverter(newCanvas->viewConverter()) , toolProxy(newCanvas->toolProxy()) , ignorenextMouseEventExceptRightMiddleClick(0) , borderColor(Qt::gray) {} QList decorations; KisCanvas2 * canvas; KisCoordinatesConverter *coordinatesConverter; const KoViewConverter * viewConverter; KoToolProxy * toolProxy; QTimer blockMouseEvent; bool ignorenextMouseEventExceptRightMiddleClick; // HACK work around Qt bug not sending tablet right/dblclick http://bugreports.qt.nokia.com/browse/QTBUG-8598 QColor borderColor; }; KisCanvasWidgetBase::KisCanvasWidgetBase(KisCanvas2 * canvas, KisCoordinatesConverter *coordinatesConverter) : m_d(new Private(canvas, coordinatesConverter)) { m_d->blockMouseEvent.setSingleShot(true); } KisCanvasWidgetBase::~KisCanvasWidgetBase() { /** - * Clear all the attached decoration. Oherwise they might decide + * Clear all the attached decoration. Otherwise they might decide * to process some events or signals after the canvas has been * destroyed */ //5qDeleteAll(m_d->decorations); m_d->decorations.clear(); delete m_d; } void KisCanvasWidgetBase::drawDecorations(QPainter & gc, const QRect &updateWidgetRect) const { if (!m_d->canvas) { dbgFile<<"canvas doesn't exist, in canvas widget base!"; return; } gc.save(); // Setup the painter to take care of the offset; all that the // classes that do painting need to keep track of is resolution gc.setRenderHint(QPainter::Antialiasing); gc.setRenderHint(QPainter::TextAntialiasing); // This option does not do anything anymore with Qt4.6, so don't re-enable it since it seems to break display // http://www.archivum.info/qt-interest@trolltech.com/2010-01/00481/Re:-(Qt-interest)-Is-QPainter::HighQualityAntialiasing-render-hint-broken-in-Qt-4.6.html // gc.setRenderHint(QPainter::HighQualityAntialiasing); gc.setRenderHint(QPainter::SmoothPixmapTransform); gc.save(); gc.setClipRect(updateWidgetRect); QTransform transform = m_d->coordinatesConverter->flakeToWidgetTransform(); gc.setTransform(transform); // Paint the shapes (other than the layers) m_d->canvas->globalShapeManager()->paint(gc, *m_d->viewConverter, false); // draw green selection outlines around text shapes that are edited, so the user sees where they end gc.save(); QTransform worldTransform = gc.worldTransform(); gc.setPen( Qt::green ); Q_FOREACH (KoShape *shape, canvas()->shapeManager()->selection()->selectedShapes()) { if (shape->shapeId() == "ArtisticText" || shape->shapeId() == "TextShapeID") { gc.setWorldTransform(shape->absoluteTransformation(m_d->viewConverter) * worldTransform); KoShape::applyConversion(gc, *m_d->viewConverter); gc.drawRect(QRectF(QPointF(), shape->size())); } } gc.restore(); // Draw text shape over canvas while editing it, that's needs to show the text selection correctly QString toolId = KoToolManager::instance()->activeToolId(); if (toolId == "ArtisticTextTool" || toolId == "TextTool") { gc.save(); gc.setPen(Qt::NoPen); gc.setBrush(Qt::NoBrush); Q_FOREACH (KoShape *shape, canvas()->shapeManager()->selection()->selectedShapes()) { if (shape->shapeId() == "ArtisticText" || shape->shapeId() == "TextShapeID") { KoShapePaintingContext paintContext(canvas(), false); gc.save(); gc.setTransform(shape->absoluteTransformation(m_d->viewConverter) * gc.transform()); canvas()->shapeManager()->paintShape(shape, gc, *m_d->viewConverter, paintContext); gc.restore(); } } gc.restore(); } gc.restore(); // ask the decorations to paint themselves Q_FOREACH (KisCanvasDecorationSP deco, m_d->decorations) { deco->paint(gc, m_d->coordinatesConverter->widgetToDocument(updateWidgetRect), m_d->coordinatesConverter,m_d->canvas); } gc.setTransform(transform); // - some tools do not restore gc, but that is not important here // - we need to disable clipping to draw handles properly gc.setClipping(false); toolProxy()->paint(gc, *m_d->viewConverter); gc.restore(); } void KisCanvasWidgetBase::addDecoration(KisCanvasDecorationSP deco) { m_d->decorations.push_back(deco); } void KisCanvasWidgetBase::removeDecoration(const QString &id) { for (auto it = m_d->decorations.begin(); it != m_d->decorations.end(); ++it) { if ((*it)->id() == id) { it = m_d->decorations.erase(it); break; } } } KisCanvasDecorationSP KisCanvasWidgetBase::decoration(const QString& id) const { Q_FOREACH (KisCanvasDecorationSP deco, m_d->decorations) { if (deco->id() == id) { return deco; } } return 0; } void KisCanvasWidgetBase::setDecorations(const QList &decorations) { m_d->decorations=decorations; } QList KisCanvasWidgetBase::decorations() const { return m_d->decorations; } void KisCanvasWidgetBase::setWrapAroundViewingMode(bool value) { Q_UNUSED(value); } QImage KisCanvasWidgetBase::createCheckersImage(qint32 checkSize) { KisConfig cfg; if(checkSize < 0) checkSize = cfg.checkSize(); QColor checkColor1 = cfg.checkersColor1(); QColor checkColor2 = cfg.checkersColor2(); QImage tile(checkSize * 2, checkSize * 2, QImage::Format_RGB32); QPainter pt(&tile); pt.fillRect(tile.rect(), checkColor2); pt.fillRect(0, 0, checkSize, checkSize, checkColor1); pt.fillRect(checkSize, checkSize, checkSize, checkSize, checkColor1); pt.end(); return tile; } void KisCanvasWidgetBase::notifyConfigChanged() { KisConfig cfg; m_d->borderColor = cfg.canvasBorderColor(); } QColor KisCanvasWidgetBase::borderColor() const { return m_d->borderColor; } KisCanvas2 *KisCanvasWidgetBase::canvas() const { return m_d->canvas; } KisCoordinatesConverter* KisCanvasWidgetBase::coordinatesConverter() const { return m_d->coordinatesConverter; } QVector KisCanvasWidgetBase::updateCanvasProjection(const QVector &infoObjects) { QVector dirtyViewRects; Q_FOREACH (KisUpdateInfoSP info, infoObjects) { dirtyViewRects << this->updateCanvasProjection(info); } return dirtyViewRects; } KoToolProxy *KisCanvasWidgetBase::toolProxy() const { return m_d->toolProxy; } QVariant KisCanvasWidgetBase::processInputMethodQuery(Qt::InputMethodQuery query) const { if (query == Qt::ImMicroFocus) { QRectF rect = m_d->toolProxy->inputMethodQuery(query, *m_d->viewConverter).toRectF(); return m_d->coordinatesConverter->flakeToWidget(rect); } return m_d->toolProxy->inputMethodQuery(query, *m_d->viewConverter); } void KisCanvasWidgetBase::processInputMethodEvent(QInputMethodEvent *event) { m_d->toolProxy->inputMethodEvent(event); } diff --git a/libs/ui/dialogs/KisAsyncAnimationRenderDialogBase.h b/libs/ui/dialogs/KisAsyncAnimationRenderDialogBase.h index f66bdf8795..a3f3cfef18 100644 --- a/libs/ui/dialogs/KisAsyncAnimationRenderDialogBase.h +++ b/libs/ui/dialogs/KisAsyncAnimationRenderDialogBase.h @@ -1,151 +1,151 @@ /* * Copyright (c) 2017 Dmitry Kazakov * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KISASYNCANIMATIONRENDERDIALOGBASE_H #define KISASYNCANIMATIONRENDERDIALOGBASE_H #include #include "kis_types.h" #include "kritaui_export.h" class KisTimeRange; class KisAsyncAnimationRendererBase; class KisViewManager; /** * @brief KisAsyncAnimationRenderDialogBase is a special class for rendering multiple * frames of the image and putting them somewhere (saving into a file or just * pushing into an openGL cache) * * The class handles a lot of boilerplate code for you and optimizes regeneration of * the frames using multithreading, according to the user's settings. The responsibilities * of the class are the following: * * Rendering itself: * - fetch the list of dirtly frames using calcDirtyFrames() * - create some clones of the image according to the user's settings * to facilitate multithreaded rendering and processing of the frames * - if the user doesn't have anough RAM, the clones will not be created * (the memory overhead is calculated using "projections" metric of the * statistics server). * - feed the images/threads with dirty frames until the all the frames * are done * * Progress reporting: * - if batchMode() is false, the user will see a progress dialog showing * the current progress with estimate about total processing timer * - the user can also cancel the regeneration by pressing Cancel button * * Usage Details: * - one should implement two methods to make the rendering work: * - calcDirtyFrames() * - createRenderer(KisImageSP image) - * - these methids will be called on the start of the rendering + * - these methods will be called on the start of the rendering */ class KRITAUI_EXPORT KisAsyncAnimationRenderDialogBase : public QObject { Q_OBJECT public: enum Result { RenderComplete, RenderCancelled, RenderFailed }; public: /** * @brief construct and initialize the dialog * @param actionTitle the first line of the status reports that the user sees in the dialog * @param image the image that will be as a source of frames. Make sure the image is *not* * locked by the time regenerateRange() is called * @param busyWait the dialog will not show for the specified time (in milliseconds) */ KisAsyncAnimationRenderDialogBase(const QString &actionTitle, KisImageSP image, int busyWait = 200); virtual ~KisAsyncAnimationRenderDialogBase(); /** * @brief start generation of frames and (if not in batch mode) show the dialog * * The link to view manager is used to barrier lock with visual feedback in the * end of the operation */ virtual Result regenerateRange(KisViewManager *viewManager); /** * Set area of image that will be regenerated. If \p roi is empty, * full area of the image is regenerated. */ void setRegionOfInterest(const QRegion &roi); /** * @see setRegionOfInterest() */ QRegion regionOfInterest() const; /** * @brief setting batch mode to true will prevent any dialogs or message boxes from * showing on screen. Please take it into account that using batch mode prevents * some potentially dangerous recovery execution paths (e.g. delete the existing * frames in the destination folder). In such case the rendering will be stopped with * RenderFailed result. */ void setBatchMode(bool value); /** * @see setBatchMode */ bool batchMode() const; private Q_SLOTS: void slotFrameCompleted(int frame); void slotFrameCancelled(int frame); void slotCancelRegeneration(); private: void tryInitiateFrameRegeneration(); void updateProgressLabel(); void cancelProcessingImpl(bool isUserCancelled); protected: /** * @brief returns a list of frames that should be regenerated by the dialog * * Called by the dialog in the very beginning of regenerateRange() */ virtual QList calcDirtyFrames() const = 0; /** * @brief create a renderer object linked to \p image * * Renderer are special objects that connect to the individual image signals, * react on them and fetch the final frames data * * @see KisAsyncAnimationRendererBase */ virtual KisAsyncAnimationRendererBase* createRenderer(KisImageSP image) = 0; virtual void initializeRendererForFrame(KisAsyncAnimationRendererBase *renderer, KisImageSP image, int frame) = 0; private: struct Private; const QScopedPointer m_d; }; #endif // KISASYNCANIMATIONRENDERDIALOGBASE_H diff --git a/libs/ui/kis_paintop_box.cc b/libs/ui/kis_paintop_box.cc index 4ae7ff9a91..8a25effc40 100644 --- a/libs/ui/kis_paintop_box.cc +++ b/libs/ui/kis_paintop_box.cc @@ -1,1347 +1,1347 @@ /* * kis_paintop_box.cc - part of KImageShop/Krayon/Krita * * Copyright (c) 2004 Boudewijn Rempt (boud@valdyas.org) * Copyright (c) 2009-2011 Sven Langkamp (sven.langkamp@gmail.com) * Copyright (c) 2010 Lukáš Tvrdý * Copyright (C) 2011 Silvio Heinrich * Copyright (C) 2011 Srikanth Tiyyagura * Copyright (c) 2014 Mohit Goyal * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_paintop_box.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_canvas2.h" #include "kis_node_manager.h" #include "KisViewManager.h" #include "kis_canvas_resource_provider.h" #include "KisResourceServerProvider.h" #include "kis_favorite_resource_manager.h" #include "kis_config.h" #include "widgets/kis_popup_button.h" #include "widgets/kis_iconwidget.h" #include "widgets/kis_tool_options_popup.h" #include "widgets/kis_paintop_presets_popup.h" #include "widgets/kis_paintop_presets_chooser_popup.h" #include "widgets/kis_workspace_chooser.h" #include "widgets/kis_paintop_list_widget.h" #include "widgets/kis_slider_spin_box.h" #include "widgets/kis_cmb_composite.h" #include "widgets/kis_widget_chooser.h" #include "tool/kis_tool.h" #include "kis_signals_blocker.h" #include "kis_action_manager.h" #include "kis_highlighted_button.h" typedef KoResourceServerSimpleConstruction > KisPaintOpPresetResourceServer; typedef KoResourceServerAdapter > KisPaintOpPresetResourceServerAdapter; KisPaintopBox::KisPaintopBox(KisViewManager *view, QWidget *parent, const char *name) : QWidget(parent) , m_resourceProvider(view->resourceProvider()) , m_optionWidget(0) , m_toolOptionsPopupButton(0) , m_brushEditorPopupButton(0) , m_presetSelectorPopupButton(0) , m_toolOptionsPopup(0) , m_viewManager(view) , m_previousNode(0) , m_currTabletToolID(KoInputDevice::invalid()) , m_presetsEnabled(true) , m_blockUpdate(false) , m_dirtyPresetsEnabled(false) , m_eraserBrushSizeEnabled(false) , m_eraserBrushOpacityEnabled(false) { Q_ASSERT(view != 0); setObjectName(name); KisConfig cfg; m_dirtyPresetsEnabled = cfg.useDirtyPresets(); m_eraserBrushSizeEnabled = cfg.useEraserBrushSize(); m_eraserBrushOpacityEnabled = cfg.useEraserBrushOpacity(); KAcceleratorManager::setNoAccel(this); setWindowTitle(i18n("Painter's Toolchest")); m_favoriteResourceManager = new KisFavoriteResourceManager(this); KConfigGroup grp = KSharedConfig::openConfig()->group("krita").group("Toolbar BrushesAndStuff"); int iconsize = grp.readEntry("IconSize", 32); if (!cfg.toolOptionsInDocker()) { m_toolOptionsPopupButton = new KisPopupButton(this); m_toolOptionsPopupButton->setIcon(KisIconUtils::loadIcon("configure")); m_toolOptionsPopupButton->setToolTip(i18n("Tool Settings")); m_toolOptionsPopupButton->setFixedSize(iconsize, iconsize); } m_brushEditorPopupButton = new KisIconWidget(this); m_brushEditorPopupButton->setIcon(KisIconUtils::loadIcon("paintop_settings_02")); m_brushEditorPopupButton->setToolTip(i18n("Edit brush settings")); m_brushEditorPopupButton->setFixedSize(iconsize, iconsize); m_presetSelectorPopupButton = new KisPopupButton(this); m_presetSelectorPopupButton->setIcon(KisIconUtils::loadIcon("paintop_settings_01")); m_presetSelectorPopupButton->setToolTip(i18n("Choose brush preset")); m_presetSelectorPopupButton->setFixedSize(iconsize, iconsize); m_eraseModeButton = new KisHighlightedToolButton(this); m_eraseModeButton->setFixedSize(iconsize, iconsize); m_eraseModeButton->setCheckable(true); m_eraseAction = m_viewManager->actionManager()->createAction("erase_action"); m_eraseModeButton->setDefaultAction(m_eraseAction); m_reloadButton = new QToolButton(this); m_reloadButton->setFixedSize(iconsize, iconsize); m_reloadAction = m_viewManager->actionManager()->createAction("reload_preset_action"); m_reloadButton->setDefaultAction(m_reloadAction); m_alphaLockButton = new KisHighlightedToolButton(this); m_alphaLockButton->setFixedSize(iconsize, iconsize); m_alphaLockButton->setCheckable(true); KisAction* alphaLockAction = m_viewManager->actionManager()->createAction("preserve_alpha"); m_alphaLockButton->setDefaultAction(alphaLockAction); // horizontal and vertical mirror toolbar buttons // mirror tool options for the X Mirror QMenu *toolbarMenuXMirror = new QMenu(); hideCanvasDecorationsX = m_viewManager->actionManager()->createAction("mirrorX-hideDecorations"); toolbarMenuXMirror->addAction(hideCanvasDecorationsX); lockActionX = m_viewManager->actionManager()->createAction("mirrorX-lock"); toolbarMenuXMirror->addAction(lockActionX); moveToCenterActionX = m_viewManager->actionManager()->createAction("mirrorX-moveToCenter"); toolbarMenuXMirror->addAction(moveToCenterActionX); // mirror tool options for the Y Mirror QMenu *toolbarMenuYMirror = new QMenu(); hideCanvasDecorationsY = m_viewManager->actionManager()->createAction("mirrorY-hideDecorations"); toolbarMenuYMirror->addAction(hideCanvasDecorationsY); lockActionY = m_viewManager->actionManager()->createAction("mirrorY-lock"); toolbarMenuYMirror->addAction(lockActionY); moveToCenterActionY = m_viewManager->actionManager()->createAction("mirrorY-moveToCenter"); toolbarMenuYMirror->addAction(moveToCenterActionY); // create horizontal and vertical mirror buttons m_hMirrorButton = new KisHighlightedToolButton(this); int menuPadding = 10; m_hMirrorButton->setFixedSize(iconsize + menuPadding, iconsize); m_hMirrorButton->setCheckable(true); m_hMirrorAction = m_viewManager->actionManager()->createAction("hmirror_action"); m_hMirrorButton->setDefaultAction(m_hMirrorAction); m_hMirrorButton->setMenu(toolbarMenuXMirror); m_hMirrorButton->setPopupMode(QToolButton::MenuButtonPopup); m_vMirrorButton = new KisHighlightedToolButton(this); m_vMirrorButton->setFixedSize(iconsize + menuPadding, iconsize); m_vMirrorButton->setCheckable(true); m_vMirrorAction = m_viewManager->actionManager()->createAction("vmirror_action"); m_vMirrorButton->setDefaultAction(m_vMirrorAction); m_vMirrorButton->setMenu(toolbarMenuYMirror); m_vMirrorButton->setPopupMode(QToolButton::MenuButtonPopup); // add connections for horizontal and mirrror buttons connect(lockActionX, SIGNAL(toggled(bool)), this, SLOT(slotLockXMirrorToggle(bool))); connect(lockActionY, SIGNAL(toggled(bool)), this, SLOT(slotLockYMirrorToggle(bool))); connect(moveToCenterActionX, SIGNAL(triggered(bool)), this, SLOT(slotMoveToCenterMirrorX())); connect(moveToCenterActionY, SIGNAL(triggered(bool)), this, SLOT(slotMoveToCenterMirrorY())); connect(hideCanvasDecorationsX, SIGNAL(toggled(bool)), this, SLOT(slotHideDecorationMirrorX(bool))); connect(hideCanvasDecorationsY, SIGNAL(toggled(bool)), this, SLOT(slotHideDecorationMirrorY(bool))); const bool sliderLabels = cfg.sliderLabels(); int sliderWidth; if (sliderLabels) { sliderWidth = 150 * logicalDpiX() / 96; } else { sliderWidth = 120 * logicalDpiX() / 96; } for (int i = 0; i < 3; ++i) { m_sliderChooser[i] = new KisWidgetChooser(i + 1); KisDoubleSliderSpinBox* slOpacity; KisDoubleSliderSpinBox* slFlow; KisDoubleSliderSpinBox* slSize; if (sliderLabels) { slOpacity = m_sliderChooser[i]->addWidget("opacity"); slFlow = m_sliderChooser[i]->addWidget("flow"); slSize = m_sliderChooser[i]->addWidget("size"); slOpacity->setPrefix(QString("%1 ").arg(i18n("Opacity:"))); slFlow->setPrefix(QString("%1 ").arg(i18n("Flow:"))); slSize->setPrefix(QString("%1 ").arg(i18n("Size:"))); } else { slOpacity = m_sliderChooser[i]->addWidget("opacity", i18n("Opacity:")); slFlow = m_sliderChooser[i]->addWidget("flow", i18n("Flow:")); slSize = m_sliderChooser[i]->addWidget("size", i18n("Size:")); } slOpacity->setRange(0, 100, 0); slOpacity->setValue(100); slOpacity->setSingleStep(5); slOpacity->setSuffix("%"); slOpacity->setMinimumWidth(qMax(sliderWidth, slOpacity->sizeHint().width())); slOpacity->setFixedHeight(iconsize); slOpacity->setBlockUpdateSignalOnDrag(true); slFlow->setRange(0, 100, 0); slFlow->setValue(100); slFlow->setSingleStep(5); slFlow->setSuffix("%"); slFlow->setMinimumWidth(qMax(sliderWidth, slFlow->sizeHint().width())); slFlow->setFixedHeight(iconsize); slFlow->setBlockUpdateSignalOnDrag(true); slSize->setRange(0, cfg.readEntry("maximumBrushSize", 1000), 2); slSize->setValue(100); slSize->setSingleStep(1); slSize->setExponentRatio(3.0); slSize->setSuffix(i18n(" px")); slSize->setMinimumWidth(qMax(sliderWidth, slSize->sizeHint().width())); slSize->setFixedHeight(iconsize); slSize->setBlockUpdateSignalOnDrag(true); m_sliderChooser[i]->chooseWidget(cfg.toolbarSlider(i + 1)); } m_cmbCompositeOp = new KisCompositeOpComboBox(); m_cmbCompositeOp->setFixedHeight(iconsize); Q_FOREACH (KisAction * a, m_cmbCompositeOp->blendmodeActions()) { m_viewManager->actionManager()->addAction(a->text(), a); } m_workspaceWidget = new KisPopupButton(this); m_workspaceWidget->setIcon(KisIconUtils::loadIcon("view-choose")); m_workspaceWidget->setToolTip(i18n("Choose workspace")); m_workspaceWidget->setFixedSize(iconsize, iconsize); m_workspaceWidget->setPopupWidget(new KisWorkspaceChooser(view)); QHBoxLayout* baseLayout = new QHBoxLayout(this); m_paintopWidget = new QWidget(this); baseLayout->addWidget(m_paintopWidget); baseLayout->setSpacing(4); baseLayout->setContentsMargins(0, 0, 0, 0); m_layout = new QHBoxLayout(m_paintopWidget); if (!cfg.toolOptionsInDocker()) { m_layout->addWidget(m_toolOptionsPopupButton); } m_layout->addWidget(m_brushEditorPopupButton); m_layout->addWidget(m_presetSelectorPopupButton); m_layout->setSpacing(4); m_layout->setContentsMargins(0, 0, 0, 0); QWidget* compositeActions = new QWidget(this); QHBoxLayout* compositeLayout = new QHBoxLayout(compositeActions); compositeLayout->addWidget(m_cmbCompositeOp); compositeLayout->addWidget(m_eraseModeButton); compositeLayout->addWidget(m_alphaLockButton); compositeLayout->setSpacing(4); compositeLayout->setContentsMargins(0, 0, 0, 0); compositeLayout->addWidget(m_reloadButton); QWidgetAction * action; action = new QWidgetAction(this); view->actionCollection()->addAction("composite_actions", action); action->setText(i18n("Brush composite")); action->setDefaultWidget(compositeActions); action = new QWidgetAction(this); KisActionRegistry::instance()->propertizeAction("brushslider1", action); view->actionCollection()->addAction("brushslider1", action); action->setDefaultWidget(m_sliderChooser[0]); connect(action, SIGNAL(triggered()), m_sliderChooser[0], SLOT(showPopupWidget())); connect(m_viewManager->mainWindow(), SIGNAL(themeChanged()), m_sliderChooser[0], SLOT(updateThemedIcons())); action = new QWidgetAction(this); KisActionRegistry::instance()->propertizeAction("brushslider2", action); view->actionCollection()->addAction("brushslider2", action); action->setDefaultWidget(m_sliderChooser[1]); connect(action, SIGNAL(triggered()), m_sliderChooser[1], SLOT(showPopupWidget())); connect(m_viewManager->mainWindow(), SIGNAL(themeChanged()), m_sliderChooser[1], SLOT(updateThemedIcons())); action = new QWidgetAction(this); KisActionRegistry::instance()->propertizeAction("brushslider3", action); view->actionCollection()->addAction("brushslider3", action); action->setDefaultWidget(m_sliderChooser[2]); connect(action, SIGNAL(triggered()), m_sliderChooser[2], SLOT(showPopupWidget())); connect(m_viewManager->mainWindow(), SIGNAL(themeChanged()), m_sliderChooser[2], SLOT(updateThemedIcons())); action = new QWidgetAction(this); KisActionRegistry::instance()->propertizeAction("next_favorite_preset", action); view->actionCollection()->addAction("next_favorite_preset", action); connect(action, SIGNAL(triggered()), this, SLOT(slotNextFavoritePreset())); action = new QWidgetAction(this); KisActionRegistry::instance()->propertizeAction("previous_favorite_preset", action); view->actionCollection()->addAction("previous_favorite_preset", action); connect(action, SIGNAL(triggered()), this, SLOT(slotPreviousFavoritePreset())); action = new QWidgetAction(this); KisActionRegistry::instance()->propertizeAction("previous_preset", action); view->actionCollection()->addAction("previous_preset", action); connect(action, SIGNAL(triggered()), this, SLOT(slotSwitchToPreviousPreset())); if (!cfg.toolOptionsInDocker()) { action = new QWidgetAction(this); KisActionRegistry::instance()->propertizeAction("show_tool_options", action); view->actionCollection()->addAction("show_tool_options", action); connect(action, SIGNAL(triggered()), m_toolOptionsPopupButton, SLOT(showPopupWidget())); } action = new QWidgetAction(this); KisActionRegistry::instance()->propertizeAction("show_brush_editor", action); view->actionCollection()->addAction("show_brush_editor", action); connect(action, SIGNAL(triggered()), m_brushEditorPopupButton, SLOT(showPopupWidget())); action = new QWidgetAction(this); KisActionRegistry::instance()->propertizeAction("show_brush_presets", action); view->actionCollection()->addAction("show_brush_presets", action); connect(action, SIGNAL(triggered()), m_presetSelectorPopupButton, SLOT(showPopupWidget())); QWidget* mirrorActions = new QWidget(this); QHBoxLayout* mirrorLayout = new QHBoxLayout(mirrorActions); mirrorLayout->addWidget(m_hMirrorButton); mirrorLayout->addWidget(m_vMirrorButton); mirrorLayout->setSpacing(4); mirrorLayout->setContentsMargins(0, 0, 0, 0); action = new QWidgetAction(this); KisActionRegistry::instance()->propertizeAction("mirror_actions", action); action->setDefaultWidget(mirrorActions); view->actionCollection()->addAction("mirror_actions", action); action = new QWidgetAction(this); KisActionRegistry::instance()->propertizeAction("workspaces", action); view->actionCollection()->addAction("workspaces", action); action->setDefaultWidget(m_workspaceWidget); if (!cfg.toolOptionsInDocker()) { m_toolOptionsPopup = new KisToolOptionsPopup(); m_toolOptionsPopupButton->setPopupWidget(m_toolOptionsPopup); m_toolOptionsPopup->switchDetached(false); } m_savePresetWidget = new KisPresetSaveWidget(this); m_presetsPopup = new KisPaintOpPresetsPopup(m_resourceProvider, m_favoriteResourceManager, m_savePresetWidget); m_brushEditorPopupButton->setPopupWidget(m_presetsPopup); m_presetsPopup->parentWidget()->setWindowTitle(i18n("Brush Editor")); connect(m_presetsPopup, SIGNAL(brushEditorShown()), SLOT(slotUpdateOptionsWidgetPopup())); connect(m_viewManager->mainWindow(), SIGNAL(themeChanged()), m_presetsPopup, SLOT(updateThemedIcons())); m_presetsChooserPopup = new KisPaintOpPresetsChooserPopup(); m_presetsChooserPopup->setMinimumHeight(550); m_presetsChooserPopup->setMinimumWidth(450); m_presetSelectorPopupButton->setPopupWidget(m_presetsChooserPopup); m_currCompositeOpID = KoCompositeOpRegistry::instance().getDefaultCompositeOp().id(); slotNodeChanged(view->activeNode()); // Get all the paintops QList keys = KisPaintOpRegistry::instance()->keys(); QList factoryList; Q_FOREACH (const QString & paintopId, keys) { factoryList.append(KisPaintOpRegistry::instance()->get(paintopId)); } m_presetsPopup->setPaintOpList(factoryList); connect(m_presetsPopup , SIGNAL(paintopActivated(QString)) , SLOT(slotSetPaintop(QString))); connect(m_presetsPopup , SIGNAL(defaultPresetClicked()) , SLOT(slotSetupDefaultPreset())); connect(m_presetsPopup , SIGNAL(signalResourceSelected(KoResource*)), SLOT(resourceSelected(KoResource*))); connect(m_presetsPopup , SIGNAL(reloadPresetClicked()) , SLOT(slotReloadPreset())); connect(m_presetsPopup , SIGNAL(dirtyPresetToggled(bool)) , SLOT(slotDirtyPresetToggled(bool))); connect(m_presetsPopup , SIGNAL(eraserBrushSizeToggled(bool)) , SLOT(slotEraserBrushSizeToggled(bool))); connect(m_presetsPopup , SIGNAL(eraserBrushOpacityToggled(bool)) , SLOT(slotEraserBrushOpacityToggled(bool))); connect(m_presetsPopup, SIGNAL(createPresetFromScratch(QString)), this, SLOT(slotCreatePresetFromScratch(QString))); connect(m_presetsChooserPopup, SIGNAL(resourceSelected(KoResource*)) , SLOT(resourceSelected(KoResource*))); connect(m_presetsChooserPopup, SIGNAL(resourceClicked(KoResource*)) , SLOT(resourceSelected(KoResource*))); connect(m_resourceProvider , SIGNAL(sigNodeChanged(const KisNodeSP)) , SLOT(slotNodeChanged(const KisNodeSP))); connect(m_cmbCompositeOp , SIGNAL(currentIndexChanged(int)) , SLOT(slotSetCompositeMode(int))); connect(m_eraseAction , SIGNAL(toggled(bool)) , SLOT(slotToggleEraseMode(bool))); connect(alphaLockAction , SIGNAL(toggled(bool)) , SLOT(slotToggleAlphaLockMode(bool))); m_disablePressureAction = m_viewManager->actionManager()->createAction("disable_pressure"); connect(m_disablePressureAction , SIGNAL(toggled(bool)) , SLOT(slotDisablePressureMode(bool))); m_disablePressureAction->setChecked(true); connect(m_hMirrorAction , SIGNAL(toggled(bool)) , SLOT(slotHorizontalMirrorChanged(bool))); connect(m_vMirrorAction , SIGNAL(toggled(bool)) , SLOT(slotVerticalMirrorChanged(bool))); connect(m_reloadAction , SIGNAL(triggered()) , SLOT(slotReloadPreset())); connect(m_sliderChooser[0]->getWidget("opacity"), SIGNAL(valueChanged(qreal)), SLOT(slotSlider1Changed())); connect(m_sliderChooser[0]->getWidget("flow") , SIGNAL(valueChanged(qreal)), SLOT(slotSlider1Changed())); connect(m_sliderChooser[0]->getWidget("size") , SIGNAL(valueChanged(qreal)), SLOT(slotSlider1Changed())); connect(m_sliderChooser[1]->getWidget("opacity"), SIGNAL(valueChanged(qreal)), SLOT(slotSlider2Changed())); connect(m_sliderChooser[1]->getWidget("flow") , SIGNAL(valueChanged(qreal)), SLOT(slotSlider2Changed())); connect(m_sliderChooser[1]->getWidget("size") , SIGNAL(valueChanged(qreal)), SLOT(slotSlider2Changed())); connect(m_sliderChooser[2]->getWidget("opacity"), SIGNAL(valueChanged(qreal)), SLOT(slotSlider3Changed())); connect(m_sliderChooser[2]->getWidget("flow") , SIGNAL(valueChanged(qreal)), SLOT(slotSlider3Changed())); connect(m_sliderChooser[2]->getWidget("size") , SIGNAL(valueChanged(qreal)), SLOT(slotSlider3Changed())); //Needed to connect canvas to favorite resource manager connect(m_viewManager->resourceProvider(), SIGNAL(sigFGColorChanged(KoColor)), SLOT(slotUnsetEraseMode())); connect(m_resourceProvider, SIGNAL(sigFGColorUsed(KoColor)), m_favoriteResourceManager, SLOT(slotAddRecentColor(KoColor))); connect(m_resourceProvider, SIGNAL(sigFGColorChanged(KoColor)), m_favoriteResourceManager, SLOT(slotChangeFGColorSelector(KoColor))); connect(m_resourceProvider, SIGNAL(sigBGColorChanged(KoColor)), m_favoriteResourceManager, SLOT(slotSetBGColor(KoColor))); // cold initialization m_favoriteResourceManager->slotChangeFGColorSelector(m_resourceProvider->fgColor()); m_favoriteResourceManager->slotSetBGColor(m_resourceProvider->bgColor()); connect(m_favoriteResourceManager, SIGNAL(sigSetFGColor(KoColor)), m_resourceProvider, SLOT(slotSetFGColor(KoColor))); connect(m_favoriteResourceManager, SIGNAL(sigSetBGColor(KoColor)), m_resourceProvider, SLOT(slotSetBGColor(KoColor))); connect(m_favoriteResourceManager, SIGNAL(sigEnableChangeColor(bool)), m_resourceProvider, SLOT(slotResetEnableFGChange(bool))); connect(view->mainWindow(), SIGNAL(themeChanged()), this, SLOT(slotUpdateSelectionIcon())); connect(m_resourceProvider->resourceManager(), SIGNAL(canvasResourceChanged(int,QVariant)), this, SLOT(slotCanvasResourceChanged(int,QVariant))); slotInputDeviceChanged(KoToolManager::instance()->currentInputDevice()); KisPaintOpPresetResourceServer *rserver = KisResourceServerProvider::instance()->paintOpPresetServer(); m_eraserName = "eraser_circle"; m_defaultPresetName = "basic_tip_default"; bool foundEraser = false; bool foundTip = false; for (int i=0; iresourceCount(); i++) { KisPaintOpPresetSP resource = rserver->resources().at(i); if (resource->name().toLower().contains("eraser_circle")) { m_eraserName = resource->name(); foundEraser = true; } else if (foundEraser == false && (resource->name().toLower().contains("eraser") || resource->filename().toLower().contains("eraser"))) { m_eraserName = resource->name(); foundEraser = true; } if (resource->name().toLower().contains("basic_tip_default")) { m_defaultPresetName = resource->name(); foundTip = true; } else if (foundTip == false && (resource->name().toLower().contains("default") || resource->filename().toLower().contains("default"))) { m_defaultPresetName = resource->name(); foundTip = true; } } } KisPaintopBox::~KisPaintopBox() { KisConfig cfg; QMapIterator iter(m_tabletToolMap); while (iter.hasNext()) { iter.next(); //qDebug() << "Writing last used preset for" << iter.key().pointer << iter.key().uniqueID << iter.value().preset->name(); if ((iter.key().pointer) == QTabletEvent::Eraser) { cfg.writeEntry(QString("LastEraser_%1").arg(iter.key().uniqueID) , iter.value().preset->name()); } else { cfg.writeEntry(QString("LastPreset_%1").arg(iter.key().uniqueID) , iter.value().preset->name()); } } // Do not delete the widget, since it is global to the application, not owned by the view m_presetsPopup->setPaintOpSettingsWidget(0); qDeleteAll(m_paintopOptionWidgets); delete m_favoriteResourceManager; for (int i = 0; i < 3; ++i) { delete m_sliderChooser[i]; } } void KisPaintopBox::restoreResource(KoResource* resource) { KisPaintOpPreset* preset = dynamic_cast(resource); if (preset) { setCurrentPaintop(preset); m_presetsPopup->setPresetImage(preset->image()); m_presetsPopup->resourceSelected(resource); } } void KisPaintopBox::newOptionWidgets(const QList > &optionWidgetList) { if (m_toolOptionsPopup) { m_toolOptionsPopup->newOptionWidgets(optionWidgetList); } } void KisPaintopBox::resourceSelected(KoResource* resource) { m_presetsPopup->setCreatingBrushFromScratch(false); // show normal UI elements when we are not creating KisPaintOpPreset* preset = dynamic_cast(resource); if (preset && preset != m_resourceProvider->currentPreset()) { if (!preset->settings()->isLoadable()) return; if (!m_dirtyPresetsEnabled) { KisSignalsBlocker blocker(m_optionWidget); if (!preset->load()) { warnKrita << "failed to load the preset."; } } //qDebug() << "resourceSelected" << resource->name(); setCurrentPaintop(preset); m_presetsPopup->setPresetImage(preset->image()); m_presetsPopup->resourceSelected(resource); } } void KisPaintopBox::setCurrentPaintop(const KoID& paintop) { KisPaintOpPresetSP preset = activePreset(paintop); Q_ASSERT(preset && preset->settings()); //qDebug() << "setCurrentPaintop();" << paintop << preset; setCurrentPaintop(preset); } void KisPaintopBox::setCurrentPaintop(KisPaintOpPresetSP preset) { //qDebug() << "setCurrentPaintop(); " << preset->name(); if (preset == m_resourceProvider->currentPreset()) { if (preset == m_tabletToolMap[m_currTabletToolID].preset) { return; } } Q_ASSERT(preset); const KoID& paintop = preset->paintOp(); m_presetConnections.clear(); if (m_resourceProvider->currentPreset()) { m_resourceProvider->setPreviousPaintOpPreset(m_resourceProvider->currentPreset()); if (m_optionWidget) { m_optionWidget->hide(); } } if (!m_paintopOptionWidgets.contains(paintop)) m_paintopOptionWidgets[paintop] = KisPaintOpRegistry::instance()->get(paintop.id())->createConfigWidget(this); m_optionWidget = m_paintopOptionWidgets[paintop]; KisSignalsBlocker b(m_optionWidget); preset->setOptionsWidget(m_optionWidget); m_optionWidget->setImage(m_viewManager->image()); m_optionWidget->setNode(m_viewManager->activeNode()); m_presetsPopup->setPaintOpSettingsWidget(m_optionWidget); m_resourceProvider->setPaintOpPreset(preset); Q_ASSERT(m_optionWidget && m_presetSelectorPopupButton); m_presetConnections.addConnection(m_optionWidget, SIGNAL(sigConfigurationUpdated()), this, SLOT(slotGuiChangedCurrentPreset())); m_presetConnections.addConnection(m_optionWidget, SIGNAL(sigSaveLockedConfig(KisPropertiesConfigurationSP)), this, SLOT(slotSaveLockedOptionToPreset(KisPropertiesConfigurationSP))); m_presetConnections.addConnection(m_optionWidget, SIGNAL(sigDropLockedConfig(KisPropertiesConfigurationSP)), this, SLOT(slotDropLockedOption(KisPropertiesConfigurationSP))); // load the current brush engine icon for the brush editor toolbar button m_brushEditorPopupButton->slotSetItem(preset.data()); m_presetsPopup->setCurrentPaintOpId(paintop.id()); ////qDebug() << "\tsetting the new preset for" << m_currTabletToolID.uniqueID << "to" << preset->name(); m_paintOpPresetMap[m_resourceProvider->currentPreset()->paintOp()] = preset; m_tabletToolMap[m_currTabletToolID].preset = preset; m_tabletToolMap[m_currTabletToolID].paintOpID = preset->paintOp(); if (m_presetsPopup->currentPaintOpId() != paintop.id()) { // Must change the paintop as the current one is not supported // by the new colorspace. dbgKrita << "current paintop " << paintop.name() << " was not set, not supported by colorspace"; } } void KisPaintopBox::slotUpdateOptionsWidgetPopup() { KisPaintOpPresetSP preset = m_resourceProvider->currentPreset(); KIS_SAFE_ASSERT_RECOVER_RETURN(preset); KIS_SAFE_ASSERT_RECOVER_RETURN(m_optionWidget); m_optionWidget->setConfigurationSafe(preset->settings()); m_presetsPopup->resourceSelected(preset.data()); m_presetsPopup->updateViewSettings(); // the m_viewManager->image() is set earlier, but the reference will be missing when the stamp button is pressed // need to later do some research on how and when we should be using weak shared pointers (WSP) that creates this situation m_optionWidget->setImage(m_viewManager->image()); } KisPaintOpPresetSP KisPaintopBox::defaultPreset(const KoID& paintOp) { QString defaultName = paintOp.id() + ".kpp"; QString path = KoResourcePaths::findResource("kis_defaultpresets", defaultName); KisPaintOpPresetSP preset = new KisPaintOpPreset(path); if (!preset->load()) { preset = KisPaintOpRegistry::instance()->defaultPreset(paintOp); } Q_ASSERT(preset); Q_ASSERT(preset->valid()); return preset; } KisPaintOpPresetSP KisPaintopBox::activePreset(const KoID& paintOp) { if (m_paintOpPresetMap[paintOp] == 0) { m_paintOpPresetMap[paintOp] = defaultPreset(paintOp); } return m_paintOpPresetMap[paintOp]; } void KisPaintopBox::updateCompositeOp(QString compositeOpID) { if (!m_optionWidget) return; KisSignalsBlocker blocker(m_optionWidget); KisNodeSP node = m_resourceProvider->currentNode(); if (node && node->paintDevice()) { if (!node->paintDevice()->colorSpace()->hasCompositeOp(compositeOpID)) compositeOpID = KoCompositeOpRegistry::instance().getDefaultCompositeOp().id(); { KisSignalsBlocker b1(m_cmbCompositeOp); m_cmbCompositeOp->selectCompositeOp(KoID(compositeOpID)); } if (compositeOpID != m_currCompositeOpID) { m_currCompositeOpID = compositeOpID; } if (compositeOpID == COMPOSITE_ERASE || m_resourceProvider->eraserMode()) { m_eraseModeButton->setChecked(true); } else { m_eraseModeButton->setChecked(false); } } } void KisPaintopBox::setWidgetState(int flags) { if (flags & (ENABLE_COMPOSITEOP | DISABLE_COMPOSITEOP)) { m_cmbCompositeOp->setEnabled(flags & ENABLE_COMPOSITEOP); m_eraseModeButton->setEnabled(flags & ENABLE_COMPOSITEOP); } if (flags & (ENABLE_PRESETS | DISABLE_PRESETS)) { m_presetSelectorPopupButton->setEnabled(flags & ENABLE_PRESETS); m_brushEditorPopupButton->setEnabled(flags & ENABLE_PRESETS); } for (int i = 0; i < 3; ++i) { if (flags & (ENABLE_OPACITY | DISABLE_OPACITY)) m_sliderChooser[i]->getWidget("opacity")->setEnabled(flags & ENABLE_OPACITY); if (flags & (ENABLE_FLOW | DISABLE_FLOW)) m_sliderChooser[i]->getWidget("flow")->setEnabled(flags & ENABLE_FLOW); if (flags & (ENABLE_SIZE | DISABLE_SIZE)) m_sliderChooser[i]->getWidget("size")->setEnabled(flags & ENABLE_SIZE); } } void KisPaintopBox::setSliderValue(const QString& sliderID, qreal value) { for (int i = 0; i < 3; ++i) { KisDoubleSliderSpinBox* slider = m_sliderChooser[i]->getWidget(sliderID); KisSignalsBlocker b(slider); if (sliderID == "opacity" || sliderID == "flow") { // opacity and flows UI stored at 0-100% slider->setValue(value*100); } else { slider->setValue(value); // brush size } } } void KisPaintopBox::slotSetPaintop(const QString& paintOpId) { if (KisPaintOpRegistry::instance()->get(paintOpId) != 0) { KoID id(paintOpId, KisPaintOpRegistry::instance()->get(paintOpId)->name()); //qDebug() << "slotsetpaintop" << id; setCurrentPaintop(id); } } void KisPaintopBox::slotInputDeviceChanged(const KoInputDevice& inputDevice) { TabletToolMap::iterator toolData = m_tabletToolMap.find(inputDevice); //qDebug() << "slotInputDeviceChanged()" << inputDevice.device() << inputDevice.uniqueTabletId(); m_currTabletToolID = TabletToolID(inputDevice); if (toolData == m_tabletToolMap.end()) { KisConfig cfg; KisPaintOpPresetResourceServer *rserver = KisResourceServerProvider::instance()->paintOpPresetServer(); KisPaintOpPresetSP preset; if (inputDevice.pointer() == QTabletEvent::Eraser) { preset = rserver->resourceByName(cfg.readEntry(QString("LastEraser_%1").arg(inputDevice.uniqueTabletId()), m_eraserName)); } else { preset = rserver->resourceByName(cfg.readEntry(QString("LastPreset_%1").arg(inputDevice.uniqueTabletId()), m_defaultPresetName)); //if (preset) //qDebug() << "found stored preset " << preset->name() << "for" << inputDevice.uniqueTabletId(); //else //qDebug() << "no preset found for" << inputDevice.uniqueTabletId(); } if (!preset) { preset = rserver->resourceByName(m_defaultPresetName); } if (preset) { //qDebug() << "inputdevicechanged 1" << preset; setCurrentPaintop(preset); } } else { if (toolData->preset) { //qDebug() << "inputdevicechanged 2" << toolData->preset; setCurrentPaintop(toolData->preset); } else { //qDebug() << "inputdevicechanged 3" << toolData->paintOpID; setCurrentPaintop(toolData->paintOpID); } } } void KisPaintopBox::slotCreatePresetFromScratch(QString paintop) { //First try to select an available default preset for that engine. If it doesn't exist, then //manually set the engine to use a new preset. KoID id(paintop, KisPaintOpRegistry::instance()->get(paintop)->name()); KisPaintOpPresetSP preset = defaultPreset(id); slotSetPaintop(paintop); // change the paintop settings area and update the UI if (!preset) { m_presetsPopup->setCreatingBrushFromScratch(true); // disable UI elements while creating from scratch preset = m_resourceProvider->currentPreset(); } else { m_resourceProvider->setPaintOpPreset(preset); preset->setOptionsWidget(m_optionWidget); } m_presetsPopup->resourceSelected(preset.data()); // this helps update the UI on the brush editor } void KisPaintopBox::slotCanvasResourceChanged(int key, const QVariant &value) { if (m_viewManager) { sender()->blockSignals(true); KisPaintOpPresetSP preset = m_viewManager->resourceProvider()->resourceManager()->resource(KisCanvasResourceProvider::CurrentPaintOpPreset).value(); if (preset && m_resourceProvider->currentPreset()->name() != preset->name()) { QString compositeOp = preset->settings()->getString("CompositeOp"); updateCompositeOp(compositeOp); resourceSelected(preset.data()); } /** * Update currently selected preset in both the popup widgets */ m_presetsChooserPopup->canvasResourceChanged(preset); m_presetsPopup->currentPresetChanged(preset); if (key == KisCanvasResourceProvider::CurrentCompositeOp) { if (m_resourceProvider->currentCompositeOp() != m_currCompositeOpID) { updateCompositeOp(m_resourceProvider->currentCompositeOp()); } } if (key == KisCanvasResourceProvider::Size) { setSliderValue("size", m_resourceProvider->size()); } if (key == KisCanvasResourceProvider::Opacity) { setSliderValue("opacity", m_resourceProvider->opacity()); } if (key == KisCanvasResourceProvider::Flow) { setSliderValue("flow", m_resourceProvider->flow()); } if (key == KisCanvasResourceProvider::EraserMode) { m_eraseAction->setChecked(value.toBool()); } if (key == KisCanvasResourceProvider::DisablePressure) { m_disablePressureAction->setChecked(value.toBool()); } sender()->blockSignals(false); } } void KisPaintopBox::slotUpdatePreset() { if (!m_resourceProvider->currentPreset()) return; // block updates of avoid some over updating of the option widget m_blockUpdate = true; setSliderValue("size", m_resourceProvider->size()); { qreal opacity = m_resourceProvider->currentPreset()->settings()->paintOpOpacity(); m_resourceProvider->setOpacity(opacity); setSliderValue("opacity", opacity); setWidgetState(ENABLE_OPACITY); } { setSliderValue("flow", m_resourceProvider->currentPreset()->settings()->paintOpFlow()); setWidgetState(ENABLE_FLOW); } { updateCompositeOp(m_resourceProvider->currentPreset()->settings()->paintOpCompositeOp()); setWidgetState(ENABLE_COMPOSITEOP); } m_blockUpdate = false; } void KisPaintopBox::slotSetupDefaultPreset() { KisPaintOpPresetSP preset = defaultPreset(m_resourceProvider->currentPreset()->paintOp()); preset->setOptionsWidget(m_optionWidget); m_resourceProvider->setPaintOpPreset(preset); // tell the brush editor that the resource has changed // so it can update everything m_presetsPopup->resourceSelected(preset.data()); } void KisPaintopBox::slotNodeChanged(const KisNodeSP node) { if (m_previousNode.isValid() && m_previousNode->paintDevice()) disconnect(m_previousNode->paintDevice().data(), SIGNAL(colorSpaceChanged(const KoColorSpace*)), this, SLOT(slotColorSpaceChanged(const KoColorSpace*))); // Reconnect colorspace change of node if (node && node->paintDevice()) { connect(node->paintDevice().data(), SIGNAL(colorSpaceChanged(const KoColorSpace*)), this, SLOT(slotColorSpaceChanged(const KoColorSpace*))); m_resourceProvider->setCurrentCompositeOp(m_currCompositeOpID); m_previousNode = node; slotColorSpaceChanged(node->colorSpace()); } if (m_optionWidget) { m_optionWidget->setNode(node); } } void KisPaintopBox::slotColorSpaceChanged(const KoColorSpace* colorSpace) { m_cmbCompositeOp->validate(colorSpace); } void KisPaintopBox::slotToggleEraseMode(bool checked) { const bool oldEraserMode = m_resourceProvider->eraserMode(); m_resourceProvider->setEraserMode(checked); if (oldEraserMode != checked && m_eraserBrushSizeEnabled) { const qreal currentSize = m_resourceProvider->size(); KisPaintOpSettingsSP settings = m_resourceProvider->currentPreset()->settings(); // remember brush size. set the eraser size to the normal brush size if not set if (checked) { settings->setSavedBrushSize(currentSize); if (qFuzzyIsNull(settings->savedEraserSize())) { settings->setSavedEraserSize(currentSize); } } else { settings->setSavedEraserSize(currentSize); if (qFuzzyIsNull(settings->savedBrushSize())) { settings->setSavedBrushSize(currentSize); } } //update value in UI (this is the main place the value is 'stored' in memory) qreal newSize = checked ? settings->savedEraserSize() : settings->savedBrushSize(); m_resourceProvider->setSize(newSize); } if (oldEraserMode != checked && m_eraserBrushOpacityEnabled) { const qreal currentOpacity = m_resourceProvider->opacity(); KisPaintOpSettingsSP settings = m_resourceProvider->currentPreset()->settings(); // remember brush opacity. set the eraser opacity to the normal brush opacity if not set if (checked) { settings->setSavedBrushOpacity(currentOpacity); if (qFuzzyIsNull(settings->savedEraserOpacity())) { settings->setSavedEraserOpacity(currentOpacity); } } else { settings->setSavedEraserOpacity(currentOpacity); if (qFuzzyIsNull(settings->savedBrushOpacity())) { settings->setSavedBrushOpacity(currentOpacity); } } //update value in UI (this is the main place the value is 'stored' in memory) qreal newOpacity = checked ? settings->savedEraserOpacity() : settings->savedBrushOpacity(); m_resourceProvider->setOpacity(newOpacity); } } void KisPaintopBox::slotSetCompositeMode(int index) { Q_UNUSED(index); QString compositeOp = m_cmbCompositeOp->selectedCompositeOp().id(); m_resourceProvider->setCurrentCompositeOp(compositeOp); } void KisPaintopBox::slotHorizontalMirrorChanged(bool value) { m_resourceProvider->setMirrorHorizontal(value); } void KisPaintopBox::slotVerticalMirrorChanged(bool value) { m_resourceProvider->setMirrorVertical(value); } void KisPaintopBox::sliderChanged(int n) { if (!m_optionWidget) // widget will not exist if the are no documents open return; KisSignalsBlocker blocker(m_optionWidget); // flow and opacity are shown as 0-100% on the UI, but their data is actually 0-1. Convert those two values // back for further work qreal opacity = m_sliderChooser[n]->getWidget("opacity")->value()/100; qreal flow = m_sliderChooser[n]->getWidget("flow")->value()/100; qreal size = m_sliderChooser[n]->getWidget("size")->value(); setSliderValue("opacity", opacity); setSliderValue("flow" , flow); setSliderValue("size" , size); if (m_presetsEnabled) { // IMPORTANT: set the PaintOp size before setting the other properties - // it wont work the other way + // it won't work the other way // TODO: why?! m_resourceProvider->setSize(size); m_resourceProvider->setOpacity(opacity); m_resourceProvider->setFlow(flow); KisLockedPropertiesProxySP propertiesProxy = KisLockedPropertiesServer::instance()->createLockedPropertiesProxy(m_resourceProvider->currentPreset()->settings()); propertiesProxy->setProperty("OpacityValue", opacity); propertiesProxy->setProperty("FlowValue", flow); m_optionWidget->setConfigurationSafe(m_resourceProvider->currentPreset()->settings().data()); } else { m_resourceProvider->setOpacity(opacity); } m_presetsPopup->resourceSelected(m_resourceProvider->currentPreset().data()); } void KisPaintopBox::slotSlider1Changed() { sliderChanged(0); } void KisPaintopBox::slotSlider2Changed() { sliderChanged(1); } void KisPaintopBox::slotSlider3Changed() { sliderChanged(2); } void KisPaintopBox::slotToolChanged(KoCanvasController* canvas, int toolId) { Q_UNUSED(canvas); Q_UNUSED(toolId); if (!m_viewManager->canvasBase()) return; QString id = KoToolManager::instance()->activeToolId(); KisTool* tool = dynamic_cast(KoToolManager::instance()->toolById(m_viewManager->canvasBase(), id)); if (tool) { int flags = tool->flags(); if (flags & KisTool::FLAG_USES_CUSTOM_COMPOSITEOP) { setWidgetState(ENABLE_COMPOSITEOP | ENABLE_OPACITY); } else { setWidgetState(DISABLE_COMPOSITEOP | DISABLE_OPACITY); } if (flags & KisTool::FLAG_USES_CUSTOM_PRESET) { setWidgetState(ENABLE_PRESETS); slotUpdatePreset(); m_presetsEnabled = true; } else { setWidgetState(DISABLE_PRESETS); m_presetsEnabled = false; } if (flags & KisTool::FLAG_USES_CUSTOM_SIZE) { setWidgetState(ENABLE_SIZE | ENABLE_FLOW); } else { setWidgetState(DISABLE_SIZE | DISABLE_FLOW); } } else setWidgetState(DISABLE_ALL); } void KisPaintopBox::slotPreviousFavoritePreset() { if (!m_favoriteResourceManager) return; QVector presets = m_favoriteResourceManager->favoritePresetList(); for (int i=0; i < presets.size(); ++i) { if (m_resourceProvider->currentPreset() && m_resourceProvider->currentPreset()->name() == presets[i]->name()) { if (i > 0) { m_favoriteResourceManager->slotChangeActivePaintop(i - 1); } else { m_favoriteResourceManager->slotChangeActivePaintop(m_favoriteResourceManager->numFavoritePresets() - 1); } //floating message should have least 2 lines, otherwise //preset thumbnail will be too small to distinguish //(because size of image on floating message depends on amount of lines in msg) m_viewManager->showFloatingMessage( i18n("%1\nselected", m_resourceProvider->currentPreset()->name()), QIcon(QPixmap::fromImage(m_resourceProvider->currentPreset()->image()))); return; } } } void KisPaintopBox::slotNextFavoritePreset() { if (!m_favoriteResourceManager) return; QVector presets = m_favoriteResourceManager->favoritePresetList(); for(int i = 0; i < presets.size(); ++i) { if (m_resourceProvider->currentPreset()->name() == presets[i]->name()) { if (i < m_favoriteResourceManager->numFavoritePresets() - 1) { m_favoriteResourceManager->slotChangeActivePaintop(i + 1); } else { m_favoriteResourceManager->slotChangeActivePaintop(0); } m_viewManager->showFloatingMessage( i18n("%1\nselected", m_resourceProvider->currentPreset()->name()), QIcon(QPixmap::fromImage(m_resourceProvider->currentPreset()->image()))); return; } } } void KisPaintopBox::slotSwitchToPreviousPreset() { if (m_resourceProvider->previousPreset()) { //qDebug() << "slotSwitchToPreviousPreset();" << m_resourceProvider->previousPreset(); setCurrentPaintop(m_resourceProvider->previousPreset()); m_viewManager->showFloatingMessage( i18n("%1\nselected", m_resourceProvider->currentPreset()->name()), QIcon(QPixmap::fromImage(m_resourceProvider->currentPreset()->image()))); } } void KisPaintopBox::slotUnsetEraseMode() { m_eraseAction->setChecked(false); } void KisPaintopBox::slotToggleAlphaLockMode(bool checked) { if (checked) { m_alphaLockButton->actions()[0]->setIcon(KisIconUtils::loadIcon("transparency-locked")); } else { m_alphaLockButton->actions()[0]->setIcon(KisIconUtils::loadIcon("transparency-unlocked")); } m_resourceProvider->setGlobalAlphaLock(checked); } void KisPaintopBox::slotDisablePressureMode(bool checked) { if (checked) { m_disablePressureAction->setIcon(KisIconUtils::loadIcon("transform_icons_penPressure")); } else { m_disablePressureAction->setIcon(KisIconUtils::loadIcon("transform_icons_penPressure_locked")); } m_resourceProvider->setDisablePressure(checked); } void KisPaintopBox::slotReloadPreset() { KisSignalsBlocker blocker(m_optionWidget); //Here using the name and fetching the preset from the server was the only way the load was working. Otherwise it was not loading. KisPaintOpPresetResourceServer * rserver = KisResourceServerProvider::instance()->paintOpPresetServer(); KisPaintOpPresetSP preset = rserver->resourceByName(m_resourceProvider->currentPreset()->name()); if (preset) { preset->load(); } } void KisPaintopBox::slotGuiChangedCurrentPreset() // Called only when UI is changed and not when preset is changed { KisPaintOpPresetSP preset = m_resourceProvider->currentPreset(); { /** * Here we postpone all the settings updates events until the entire writing * operation will be finished. As soon as it is finished, the updates will be * emitted happily (if there were any). */ KisPaintOpPreset::UpdatedPostponer postponer(preset.data()); QStringList preserveProperties; preserveProperties << "lodUserAllowed"; preserveProperties << "lodSizeThreshold"; // clear all the properties before dumping the stuff into the preset, // some of the options add the values incrementally // (e.g. KisPaintOpUtils::RequiredBrushFilesListTag), therefore they // may add up if we pass the same preset multiple times preset->settings()->resetSettings(preserveProperties); m_optionWidget->writeConfigurationSafe(const_cast(preset->settings().data())); } // we should also update the preset strip to update the status of the "dirty" mark m_presetsPopup->resourceSelected(m_resourceProvider->currentPreset().data()); // TODO!!!!!!!! //m_presetsPopup->updateViewSettings(); } void KisPaintopBox::slotSaveLockedOptionToPreset(KisPropertiesConfigurationSP p) { QMapIterator i(p->getProperties()); while (i.hasNext()) { i.next(); m_resourceProvider->currentPreset()->settings()->setProperty(i.key(), QVariant(i.value())); if (m_resourceProvider->currentPreset()->settings()->hasProperty(i.key() + "_previous")) { m_resourceProvider->currentPreset()->settings()->removeProperty(i.key() + "_previous"); } } slotGuiChangedCurrentPreset(); } void KisPaintopBox::slotDropLockedOption(KisPropertiesConfigurationSP p) { KisSignalsBlocker blocker(m_optionWidget); KisPaintOpPresetSP preset = m_resourceProvider->currentPreset(); { KisPaintOpPreset::DirtyStateSaver dirtySaver(preset.data()); QMapIterator i(p->getProperties()); while (i.hasNext()) { i.next(); if (preset->settings()->hasProperty(i.key() + "_previous")) { preset->settings()->setProperty(i.key(), preset->settings()->getProperty(i.key() + "_previous")); preset->settings()->removeProperty(i.key() + "_previous"); } } } //slotUpdatePreset(); } void KisPaintopBox::slotDirtyPresetToggled(bool value) { if (!value) { slotReloadPreset(); m_presetsPopup->resourceSelected(m_resourceProvider->currentPreset().data()); m_presetsPopup->updateViewSettings(); } m_dirtyPresetsEnabled = value; KisConfig cfg; cfg.setUseDirtyPresets(m_dirtyPresetsEnabled); } void KisPaintopBox::slotEraserBrushSizeToggled(bool value) { m_eraserBrushSizeEnabled = value; KisConfig cfg; cfg.setUseEraserBrushSize(m_eraserBrushSizeEnabled); } void KisPaintopBox::slotEraserBrushOpacityToggled(bool value) { m_eraserBrushOpacityEnabled = value; KisConfig cfg; cfg.setUseEraserBrushOpacity(m_eraserBrushOpacityEnabled); } void KisPaintopBox::slotUpdateSelectionIcon() { m_hMirrorAction->setIcon(KisIconUtils::loadIcon("symmetry-horizontal")); m_vMirrorAction->setIcon(KisIconUtils::loadIcon("symmetry-vertical")); KisConfig cfg; if (!cfg.toolOptionsInDocker() && m_toolOptionsPopupButton) { m_toolOptionsPopupButton->setIcon(KisIconUtils::loadIcon("configure")); } m_presetSelectorPopupButton->setIcon(KisIconUtils::loadIcon("paintop_settings_01")); m_brushEditorPopupButton->setIcon(KisIconUtils::loadIcon("paintop_settings_02")); m_workspaceWidget->setIcon(KisIconUtils::loadIcon("view-choose")); m_eraseAction->setIcon(KisIconUtils::loadIcon("draw-eraser")); m_reloadAction->setIcon(KisIconUtils::loadIcon("view-refresh")); if (m_disablePressureAction->isChecked()) { m_disablePressureAction->setIcon(KisIconUtils::loadIcon("transform_icons_penPressure")); } else { m_disablePressureAction->setIcon(KisIconUtils::loadIcon("transform_icons_penPressure_locked")); } } void KisPaintopBox::slotLockXMirrorToggle(bool toggleLock) { m_resourceProvider->setMirrorHorizontalLock(toggleLock); } void KisPaintopBox::slotLockYMirrorToggle(bool toggleLock) { m_resourceProvider->setMirrorVerticalLock(toggleLock); } void KisPaintopBox::slotHideDecorationMirrorX(bool toggled) { m_resourceProvider->setMirrorHorizontalHideDecorations(toggled); } void KisPaintopBox::slotHideDecorationMirrorY(bool toggled) { m_resourceProvider->setMirrorVerticalHideDecorations(toggled); } void KisPaintopBox::slotMoveToCenterMirrorX() { m_resourceProvider->mirrorHorizontalMoveCanvasToCenter(); } void KisPaintopBox::slotMoveToCenterMirrorY() { m_resourceProvider->mirrorVerticalMoveCanvasToCenter(); } diff --git a/libs/ui/tests/modeltest.cpp b/libs/ui/tests/modeltest.cpp index fc5948f573..3a940f1e44 100644 --- a/libs/ui/tests/modeltest.cpp +++ b/libs/ui/tests/modeltest.cpp @@ -1,545 +1,545 @@ /**************************************************************************** ** ** Copyright (C) 2007 Trolltech ASA. All rights reserved. ** ** This file is part of the Qt Concurrent project on Trolltech Labs. ** ** This file may be used under the terms of the GNU General Public ** License version 2.0 as published by the Free Software Foundation ** and appearing in the file LICENSE.GPL included in the packaging of ** this file. Please review the following information to ensure GNU ** General Public Licensing requirements will be met: ** http://www.trolltech.com/products/qt/opensource.html ** ** If you are unsure which license is appropriate for your use, please ** review the following information: ** http://www.trolltech.com/products/qt/licensing.html or contact the ** sales department at sales@trolltech.com. ** ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. ** ****************************************************************************/ #include #include "modeltest.h" Q_DECLARE_METATYPE(QModelIndex) /*! Connect to all of the models signals. Whenever anything happens recheck everything. */ ModelTest::ModelTest(QAbstractItemModel *_model, QObject *parent) : QObject(parent), model(_model), fetchingMore(false) { Q_ASSERT(model); connect(model, SIGNAL(columnsAboutToBeInserted(QModelIndex,int,int)), this, SLOT(runAllTests())); connect(model, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)), this, SLOT(runAllTests())); connect(model, SIGNAL(columnsInserted(QModelIndex,int,int)), this, SLOT(runAllTests())); connect(model, SIGNAL(columnsRemoved(QModelIndex,int,int)), this, SLOT(runAllTests())); connect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(runAllTests())); connect(model, SIGNAL(headerDataChanged(Qt::Orientation,int,int)), this, SLOT(runAllTests())); connect(model, SIGNAL(layoutAboutToBeChanged()), this, SLOT(runAllTests())); connect(model, SIGNAL(layoutChanged()), this, SLOT(runAllTests())); connect(model, SIGNAL(modelReset()), this, SLOT(runAllTests())); connect(model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), this, SLOT(runAllTests())); connect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), this, SLOT(runAllTests())); connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(runAllTests())); connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(runAllTests())); // Special checks for inserting/removing connect(model, SIGNAL(layoutAboutToBeChanged()), this, SLOT(layoutAboutToBeChanged())); connect(model, SIGNAL(layoutChanged()), this, SLOT(layoutChanged())); connect(model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), this, SLOT(rowsAboutToBeInserted(QModelIndex,int,int))); connect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), this, SLOT(rowsAboutToBeRemoved(QModelIndex,int,int))); connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(rowsInserted(QModelIndex,int,int))); connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(rowsRemoved(QModelIndex,int,int))); runAllTests(); } void ModelTest::runAllTests() { if (fetchingMore) return; nonDestructiveBasicTest(); rowCount(); columnCount(); hasIndex(); index(); parent(); data(); } /*! nonDestructiveBasicTest tries to call a number of the basic functions (not all) to make sure the model doesn't outright segfault, testing the functions that makes sense. */ void ModelTest::nonDestructiveBasicTest() { Q_ASSERT(model->buddy(QModelIndex()) == QModelIndex()); model->canFetchMore(QModelIndex()); Q_ASSERT(model->columnCount(QModelIndex()) >= 0); Q_ASSERT(model->data(QModelIndex()) == QVariant()); fetchingMore = true; model->fetchMore(QModelIndex()); fetchingMore = false; Qt::ItemFlags flags = model->flags(QModelIndex()); Q_ASSERT(flags == Qt::ItemIsDropEnabled || flags == 0); Q_UNUSED(flags); model->hasChildren(QModelIndex()); model->hasIndex(0, 0); model->headerData(0, Qt::Horizontal); model->index(0, 0); model->itemData(QModelIndex()); QVariant cache; model->match(QModelIndex(), -1, cache); model->mimeTypes(); Q_ASSERT(model->parent(QModelIndex()) == QModelIndex()); Q_ASSERT(model->rowCount() >= 0); QVariant variant; model->setData(QModelIndex(), variant, -1); model->setHeaderData(-1, Qt::Horizontal, QVariant()); model->setHeaderData(0, Qt::Horizontal, QVariant()); model->setHeaderData(999999, Qt::Horizontal, QVariant()); QMap roles; model->sibling(0, 0, QModelIndex()); model->span(QModelIndex()); model->supportedDropActions(); } /*! Tests model's implementation of QAbstractItemModel::rowCount() and hasChildren() Models that are dynamically populated are not as fully tested here. */ void ModelTest::rowCount() { // check top row QModelIndex topIndex = model->index(0, 0, QModelIndex()); int rows = model->rowCount(topIndex); Q_ASSERT(rows >= 0); if (rows > 0) Q_ASSERT(model->hasChildren(topIndex) == true); QModelIndex secondLevelIndex = model->index(0, 0, topIndex); if (secondLevelIndex.isValid()) { // not the top level // check a row count where parent is valid rows = model->rowCount(secondLevelIndex); Q_ASSERT(rows >= 0); if (rows > 0) Q_ASSERT(model->hasChildren(secondLevelIndex) == true); } // The models rowCount() is tested more extensively in checkChildren(), // but this catches the big mistakes } /*! Tests model's implementation of QAbstractItemModel::columnCount() and hasChildren() */ void ModelTest::columnCount() { // check top row QModelIndex topIndex = model->index(0, 0, QModelIndex()); Q_ASSERT(model->columnCount(topIndex) >= 0); // check a column count where parent is valid QModelIndex childIndex = model->index(0, 0, topIndex); if (childIndex.isValid()) Q_ASSERT(model->columnCount(childIndex) >= 0); // columnCount() is tested more extensively in checkChildren(), // but this catches the big mistakes } /*! Tests model's implementation of QAbstractItemModel::hasIndex() */ void ModelTest::hasIndex() { // Make sure that invalid values returns an invalid index Q_ASSERT(model->hasIndex(-2, -2) == false); Q_ASSERT(model->hasIndex(-2, 0) == false); Q_ASSERT(model->hasIndex(0, -2) == false); int rows = model->rowCount(); int columns = model->columnCount(); Q_UNUSED(columns); // check out of bounds Q_ASSERT(model->hasIndex(rows, columns) == false); Q_ASSERT(model->hasIndex(rows + 1, columns + 1) == false); if (rows > 0) Q_ASSERT(model->hasIndex(0, 0) == true); // hasIndex() is tested more extensively in checkChildren(), // but this catches the big mistakes } /*! Tests model's implementation of QAbstractItemModel::index() */ void ModelTest::index() { // Make sure that invalid values returns an invalid index Q_ASSERT(model->index(-2, -2) == QModelIndex()); Q_ASSERT(model->index(-2, 0) == QModelIndex()); Q_ASSERT(model->index(0, -2) == QModelIndex()); int rows = model->rowCount(); int columns = model->columnCount(); Q_UNUSED(columns); if (rows == 0) return; // Catch off by one errors Q_ASSERT(model->index(rows, columns) == QModelIndex()); Q_ASSERT(model->index(0, 0).isValid() == true); // Make sure that the same index is *always* returned QModelIndex a = model->index(0, 0); QModelIndex b = model->index(0, 0); Q_ASSERT(a == b); // index() is tested more extensively in checkChildren(), // but this catches the big mistakes } /*! Tests model's implementation of QAbstractItemModel::parent() */ void ModelTest::parent() { - // Make sure the model wont crash and will return an invalid QModelIndex + // Make sure the model won't crash and will return an invalid QModelIndex // when asked for the parent of an invalid index. Q_ASSERT(model->parent(QModelIndex()) == QModelIndex()); if (model->rowCount() == 0) return; // Column 0 | Column 1 | // QModelIndex() | | // \- topIndex | topIndex1 | // \- childIndex | childIndex1 | // Common error test #1, make sure that a top level index has a parent // that is a invalid QModelIndex. QModelIndex topIndex = model->index(0, 0, QModelIndex()); Q_ASSERT(model->parent(topIndex) == QModelIndex()); // Common error test #2, make sure that a second level index has a parent // that is the first level index. if (model->rowCount(topIndex) > 0) { QModelIndex childIndex = model->index(0, 0, topIndex); Q_ASSERT(model->parent(childIndex) == topIndex); } // Common error test #3, the second column should NOT have the same children // as the first column in a row. // Usually the second column shouldn't have children. QModelIndex topIndex1 = model->index(0, 1, QModelIndex()); if (model->rowCount(topIndex1) > 0) { QModelIndex childIndex = model->index(0, 0, topIndex); QModelIndex childIndex1 = model->index(0, 0, topIndex1); Q_ASSERT(childIndex != childIndex1); } // Full test, walk n levels deep through the model making sure that all // parent's children correctly specify their parent. checkChildren(QModelIndex()); } /*! Called from the parent() test. A model that returns an index of parent X should also return X when asking for the parent of the index. This recursive function does pretty extensive testing on the whole model in an effort to catch edge cases. This function assumes that rowCount(), columnCount() and index() already work. If they have a bug it will point it out, but the above tests should have already found the basic bugs because it is easier to figure out the problem in those tests then this one. */ void ModelTest::checkChildren(const QModelIndex &parent, int currentDepth) { // First just try walking back up the tree. QModelIndex p = parent; while (p.isValid()) p = p.parent(); // For models that are dynamically populated if (model->canFetchMore(parent)) { fetchingMore = true; model->fetchMore(parent); fetchingMore = false; } int rows = model->rowCount(parent); int columns = model->columnCount(parent); if (rows > 0) Q_ASSERT(model->hasChildren(parent)); // Some further testing against rows(), columns(), and hasChildren() Q_ASSERT(rows >= 0); Q_ASSERT(columns >= 0); if (rows > 0) Q_ASSERT(model->hasChildren(parent) == true); //dbgKrita << "parent:" << model->data(parent).toString() << "rows:" << rows // << "columns:" << columns << "parent column:" << parent.column(); Q_ASSERT(model->hasIndex(rows + 1, 0, parent) == false); for (int r = 0; r < rows; ++r) { if (model->canFetchMore(parent)) { fetchingMore = true; model->fetchMore(parent); fetchingMore = false; } Q_ASSERT(model->hasIndex(r, columns + 1, parent) == false); for (int c = 0; c < columns; ++c) { Q_ASSERT(model->hasIndex(r, c, parent) == true); QModelIndex index = model->index(r, c, parent); // rowCount() and columnCount() said that it existed... Q_ASSERT(index.isValid() == true); // index() should always return the same index when called twice in a row QModelIndex modifiedIndex = model->index(r, c, parent); Q_ASSERT(index == modifiedIndex); // Make sure we get the same index if we request it twice in a row QModelIndex a = model->index(r, c, parent); QModelIndex b = model->index(r, c, parent); Q_ASSERT(a == b); // Some basic checking on the index that is returned Q_ASSERT(index.model() == model); Q_ASSERT(index.row() == r); Q_ASSERT(index.column() == c); // While you can technically return a QVariant usually this is a sign // of an bug in data() Disable if this really is ok in your model. Q_ASSERT(model->data(index, Qt::DisplayRole).isValid() == true); // If the next test fails here is some somewhat useful debug you play with. /* if (model->parent(index) != parent) { dbgKrita << r << c << currentDepth << model->data(index).toString() << model->data(parent).toString(); dbgKrita << index << parent << model->parent(index); // And a view that you can even use to show the model. //QTreeView view; //view.setModel(model); //view.show(); }*/ // Check that we can get back our real parent. Q_ASSERT(model->parent(index) == parent); // recursively go down the children if (model->hasChildren(index) && currentDepth < 10) { //dbgKrita << r << c << "has children" << model->rowCount(index); checkChildren(index, ++currentDepth); }/* else { if (currentDepth >= 10) dbgKrita << "checked 10 deep"; };*/ // make sure that after testing the children that the index doesn't change. QModelIndex newerIndex = model->index(r, c, parent); Q_ASSERT(index == newerIndex); } } } /*! Tests model's implementation of QAbstractItemModel::data() */ void ModelTest::data() { // Invalid index should return an invalid qvariant Q_ASSERT(!model->data(QModelIndex()).isValid()); if (model->rowCount() == 0) return; // A valid index should have a valid QVariant data Q_ASSERT(model->index(0, 0).isValid()); // shouldn't be able to set data on an invalid index Q_ASSERT(model->setData(QModelIndex(), QLatin1String("foo"), Qt::DisplayRole) == false); // General Purpose roles that should return a QString QVariant variant = model->data(model->index(0, 0), Qt::ToolTipRole); if (variant.isValid()) { Q_ASSERT(variant.canConvert()); } variant = model->data(model->index(0, 0), Qt::StatusTipRole); if (variant.isValid()) { Q_ASSERT(variant.canConvert()); } variant = model->data(model->index(0, 0), Qt::WhatsThisRole); if (variant.isValid()) { Q_ASSERT(variant.canConvert()); } // General Purpose roles that should return a QSize variant = model->data(model->index(0, 0), Qt::SizeHintRole); if (variant.isValid()) { Q_ASSERT(variant.canConvert()); } // General Purpose roles that should return a QFont QVariant fontVariant = model->data(model->index(0, 0), Qt::FontRole); if (fontVariant.isValid()) { Q_ASSERT(fontVariant.canConvert()); } // Check that the alignment is one we know about QVariant textAlignmentVariant = model->data(model->index(0, 0), Qt::TextAlignmentRole); if (textAlignmentVariant.isValid()) { int alignment = textAlignmentVariant.toInt(); Q_UNUSED(alignment); Q_ASSERT(alignment == Qt::AlignLeft || alignment == Qt::AlignRight || alignment == Qt::AlignHCenter || alignment == Qt::AlignJustify || alignment == Qt::AlignTop || alignment == Qt::AlignBottom || alignment == Qt::AlignVCenter || alignment == Qt::AlignCenter || alignment == Qt::AlignAbsolute || alignment == Qt::AlignLeading || alignment == Qt::AlignTrailing); } // General Purpose roles that should return a QColor QVariant colorVariant = model->data(model->index(0, 0), Qt::BackgroundColorRole); if (colorVariant.isValid()) { Q_ASSERT(colorVariant.canConvert()); } colorVariant = model->data(model->index(0, 0), Qt::TextColorRole); if (colorVariant.isValid()) { Q_ASSERT(colorVariant.canConvert()); } // Check that the "check state" is one we know about. QVariant checkStateVariant = model->data(model->index(0, 0), Qt::CheckStateRole); if (checkStateVariant.isValid()) { int state = checkStateVariant.toInt(); Q_UNUSED(state); Q_ASSERT(state == Qt::Unchecked || state == Qt::PartiallyChecked || state == Qt::Checked); } } /*! Store what is about to be inserted to make sure it actually happens \sa rowsInserted() */ void ModelTest::rowsAboutToBeInserted(const QModelIndex &parent, int start, int end) { Q_UNUSED(end); Changing c; c.parent = parent; c.oldSize = model->rowCount(parent); c.last = model->data(model->index(start - 1, 0, parent)); c.next = model->data(model->index(start, 0, parent)); insert.push(c); } /*! Confirm that what was said was going to happen actually did \sa rowsAboutToBeInserted() */ void ModelTest::rowsInserted(const QModelIndex & parent, int start, int end) { Q_UNUSED(parent); Q_UNUSED(start); Q_UNUSED(end); Changing c = insert.pop(); Q_ASSERT(c.parent == parent); Q_ASSERT(c.oldSize + (end - start + 1) == model->rowCount(parent)); Q_ASSERT(c.last == model->data(model->index(start - 1, 0, c.parent))); /* if (c.next != model->data(model->index(end + 1, 0, c.parent))) { dbgKrita << start << end; for (int i=0; i < model->rowCount(); ++i) dbgKrita << model->index(i, 0).data().toString(); dbgKrita << c.next << model->data(model->index(end + 1, 0, c.parent)); } */ Q_ASSERT(c.next == model->data(model->index(end + 1, 0, c.parent))); } void ModelTest::layoutAboutToBeChanged() { for (int i = 0; i < qBound(0, model->rowCount(), 100); ++i) changing.append(QPersistentModelIndex(model->index(i, 0))); } void ModelTest::layoutChanged() { for (int i = 0; i < changing.count(); ++i) { QPersistentModelIndex p = changing[i]; Q_ASSERT(p == model->index(p.row(), p.column(), p.parent())); } changing.clear(); } /*! Store what is about to be inserted to make sure it actually happens \sa rowsRemoved() */ void ModelTest::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) { Changing c; c.parent = parent; c.oldSize = model->rowCount(parent); c.last = model->data(model->index(start - 1, 0, parent)); c.next = model->data(model->index(end + 1, 0, parent)); remove.push(c); } /*! Confirm that what was said was going to happen actually did \sa rowsAboutToBeRemoved() */ void ModelTest::rowsRemoved(const QModelIndex & parent, int start, int end) { Q_UNUSED(parent); Q_UNUSED(start); Q_UNUSED(end); Changing c = remove.pop(); Q_ASSERT(c.parent == parent); Q_ASSERT(c.oldSize - (end - start + 1) == model->rowCount(parent)); Q_ASSERT(c.last == model->data(model->index(start - 1, 0, c.parent))); Q_ASSERT(c.next == model->data(model->index(start, 0, c.parent))); } diff --git a/libs/ui/tool/kis_tool_paint.cc b/libs/ui/tool/kis_tool_paint.cc index 23ecd60c0c..deb4e3b0b2 100644 --- a/libs/ui/tool/kis_tool_paint.cc +++ b/libs/ui/tool/kis_tool_paint.cc @@ -1,801 +1,801 @@ /* * Copyright (c) 2003-2009 Boudewijn Rempt * Copyright (c) 2015 Moritz Molch * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_tool_paint.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_display_color_converter.h" #include #include #include "kis_config.h" #include "kis_config_notifier.h" #include "kis_cursor.h" #include "widgets/kis_cmb_composite.h" #include "widgets/kis_slider_spin_box.h" #include "kis_canvas_resource_provider.h" #include "kis_tool_utils.h" #include #include #include #include #include "strokes/kis_color_picker_stroke_strategy.h" KisToolPaint::KisToolPaint(KoCanvasBase *canvas, const QCursor &cursor) : KisTool(canvas, cursor), m_showColorPreview(false), m_colorPreviewShowComparePlate(false), m_colorPickerDelayTimer(), m_isOutlineEnabled(true) { m_specialHoverModifier = false; m_optionsWidgetLayout = 0; m_opacity = OPACITY_OPAQUE_U8; m_supportOutline = false; { int maxSize = KisConfig().readEntry("maximumBrushSize", 1000); int brushSize = 1; do { m_standardBrushSizes.push_back(brushSize); int increment = qMax(1, int(std::ceil(qreal(brushSize) / 15))); brushSize += increment; } while (brushSize < maxSize); m_standardBrushSizes.push_back(maxSize); } KisCanvas2 *kiscanvas = dynamic_cast(canvas); KisActionManager *actionManager = kiscanvas->viewManager()->actionManager(); // XXX: Perhaps a better place for these? if (!actionManager->actionByName("increase_brush_size")) { KisAction *increaseBrushSize = new KisAction(i18n("Increase Brush Size")); increaseBrushSize->setShortcut(Qt::Key_BracketRight); actionManager->addAction("increase_brush_size", increaseBrushSize); } if (!actionManager->actionByName("decrease_brush_size")) { KisAction *decreaseBrushSize = new KisAction(i18n("Decrease Brush Size")); decreaseBrushSize->setShortcut(Qt::Key_BracketLeft); actionManager->addAction("decrease_brush_size", decreaseBrushSize); } addAction("increase_brush_size", dynamic_cast(actionManager->actionByName("increase_brush_size"))); addAction("decrease_brush_size", dynamic_cast(actionManager->actionByName("decrease_brush_size"))); connect(this, SIGNAL(sigPaintingFinished()), kiscanvas->viewManager()->resourceProvider(), SLOT(slotPainting())); m_colorPickerDelayTimer.setSingleShot(true); connect(&m_colorPickerDelayTimer, SIGNAL(timeout()), this, SLOT(activatePickColorDelayed())); using namespace std::placeholders; // For _1 placeholder std::function callback = std::bind(&KisToolPaint::addPickerJob, this, _1); m_colorPickingCompressor.reset( new PickingCompressor(100, callback, KisSignalCompressor::FIRST_ACTIVE)); } KisToolPaint::~KisToolPaint() { } int KisToolPaint::flags() const { return KisTool::FLAG_USES_CUSTOM_COMPOSITEOP; } void KisToolPaint::canvasResourceChanged(int key, const QVariant& v) { KisTool::canvasResourceChanged(key, v); switch(key) { case(KisCanvasResourceProvider::Opacity): setOpacity(v.toDouble()); break; default: //nothing break; } connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(resetCursorStyle()), Qt::UniqueConnection); } void KisToolPaint::activate(ToolActivation toolActivation, const QSet &shapes) { if (currentPaintOpPreset()) { QString formattedBrushName = currentPaintOpPreset()->name().replace("_", " "); emit statusTextChanged(formattedBrushName); } KisTool::activate(toolActivation, shapes); if (flags() & KisTool::FLAG_USES_CUSTOM_SIZE) { connect(action("increase_brush_size"), SIGNAL(triggered()), SLOT(increaseBrushSize()), Qt::UniqueConnection); connect(action("decrease_brush_size"), SIGNAL(triggered()), SLOT(decreaseBrushSize()), Qt::UniqueConnection); } KisCanvasResourceProvider *provider = qobject_cast(canvas())->viewManager()->resourceProvider(); m_oldOpacity = provider->opacity(); provider->setOpacity(m_localOpacity); } void KisToolPaint::deactivate() { if (flags() & KisTool::FLAG_USES_CUSTOM_SIZE) { disconnect(action("increase_brush_size"), 0, this, 0); disconnect(action("decrease_brush_size"), 0, this, 0); } KisCanvasResourceProvider *provider = qobject_cast(canvas())->viewManager()->resourceProvider(); m_localOpacity = provider->opacity(); provider->setOpacity(m_oldOpacity); KisTool::deactivate(); } QPainterPath KisToolPaint::tryFixBrushOutline(const QPainterPath &originalOutline) { KisConfig cfg; if (cfg.newOutlineStyle() == OUTLINE_NONE) return originalOutline; const qreal minThresholdSize = cfg.outlineSizeMinimum(); /** * If the brush outline is bigger than the canvas itself (which * would make it invisible for a user in most of the cases) just * add a cross in the center of it */ QSize widgetSize = canvas()->canvasWidget()->size(); const int maxThresholdSum = widgetSize.width() + widgetSize.height(); QPainterPath outline = originalOutline; QRectF boundingRect = outline.boundingRect(); const qreal sum = boundingRect.width() + boundingRect.height(); QPointF center = boundingRect.center(); if (sum > maxThresholdSum) { const int hairOffset = 7; outline.moveTo(center.x(), center.y() - hairOffset); outline.lineTo(center.x(), center.y() + hairOffset); outline.moveTo(center.x() - hairOffset, center.y()); outline.lineTo(center.x() + hairOffset, center.y()); } else if (sum < minThresholdSize && !outline.isEmpty()) { outline = QPainterPath(); outline.addEllipse(center, 0.5 * minThresholdSize, 0.5 * minThresholdSize); } return outline; } void KisToolPaint::paint(QPainter &gc, const KoViewConverter &converter) { Q_UNUSED(converter); QPainterPath path = tryFixBrushOutline(pixelToView(m_currentOutline)); paintToolOutline(&gc, path); if (m_showColorPreview) { QRectF viewRect = converter.documentToView(m_oldColorPreviewRect); gc.fillRect(viewRect, m_colorPreviewCurrentColor); if (m_colorPreviewShowComparePlate) { QRectF baseColorRect = viewRect.translated(viewRect.width(), 0); gc.fillRect(baseColorRect, m_colorPreviewBaseColor); } } } void KisToolPaint::setMode(ToolMode mode) { if(this->mode() == KisTool::PAINT_MODE && mode != KisTool::PAINT_MODE) { // Let's add history information about recently used colors emit sigPaintingFinished(); } KisTool::setMode(mode); } void KisToolPaint::activatePickColor(AlternateAction action) { m_showColorPreview = true; requestUpdateOutline(m_outlineDocPoint, 0); int resource = colorPreviewResourceId(action); KoColor color = canvas()->resourceManager()->koColorResource(resource); KisCanvas2 * kisCanvas = dynamic_cast(canvas()); KIS_ASSERT_RECOVER_RETURN(kisCanvas); m_colorPreviewCurrentColor = kisCanvas->displayColorConverter()->toQColor(color); if (!m_colorPreviewBaseColor.isValid()) { m_colorPreviewBaseColor = m_colorPreviewCurrentColor; } } void KisToolPaint::deactivatePickColor(AlternateAction action) { Q_UNUSED(action); m_showColorPreview = false; m_oldColorPreviewRect = QRect(); m_oldColorPreviewUpdateRect = QRect(); m_colorPreviewCurrentColor = QColor(); } void KisToolPaint::pickColorWasOverridden() { m_colorPreviewShowComparePlate = false; m_colorPreviewBaseColor = QColor(); } void KisToolPaint::activateAlternateAction(AlternateAction action) { switch (action) { case PickFgNode: /* Falls through */ case PickBgNode: /* Falls through */ case PickFgImage: /* Falls through */ case PickBgImage: delayedAction = action; m_colorPickerDelayTimer.start(100); /* Falls through */ default: pickColorWasOverridden(); KisTool::activateAlternateAction(action); }; } void KisToolPaint::activatePickColorDelayed() { switch (delayedAction) { case PickFgNode: useCursor(KisCursor::pickerLayerForegroundCursor()); activatePickColor(delayedAction); break; case PickBgNode: useCursor(KisCursor::pickerLayerBackgroundCursor()); activatePickColor(delayedAction); break; case PickFgImage: useCursor(KisCursor::pickerImageForegroundCursor()); activatePickColor(delayedAction); break; case PickBgImage: useCursor(KisCursor::pickerImageBackgroundCursor()); activatePickColor(delayedAction); break; default: break; }; repaintDecorations(); } bool KisToolPaint::isPickingAction(AlternateAction action) { return action == PickFgNode || action == PickBgNode || action == PickFgImage || action == PickBgImage; } void KisToolPaint::deactivateAlternateAction(AlternateAction action) { if (!isPickingAction(action)) { KisTool::deactivateAlternateAction(action); return; } delayedAction = KisTool::NONE; m_colorPickerDelayTimer.stop(); resetCursorStyle(); deactivatePickColor(action); } void KisToolPaint::addPickerJob(const PickingJob &pickingJob) { /** * The actual picking is delayed by a compressor, so we can get this * event when the stroke is already closed */ if (!m_pickerStrokeId) return; KIS_ASSERT_RECOVER_RETURN(isPickingAction(pickingJob.action)); const QPoint imagePoint = image()->documentToImagePixelFloored(pickingJob.documentPixel); const bool fromCurrentNode = pickingJob.action == PickFgNode || pickingJob.action == PickBgNode; m_pickingResource = colorPreviewResourceId(pickingJob.action); if (!fromCurrentNode) { auto *kisCanvas = dynamic_cast(canvas()); KIS_SAFE_ASSERT_RECOVER_RETURN(kisCanvas); KisSharedPtr referencesLayer = kisCanvas->imageView()->document()->referenceImagesLayer(); if (referencesLayer && kisCanvas->referenceImagesDecoration()->visible()) { QColor color = referencesLayer->getPixel(imagePoint); if (color.isValid() && color.alpha() != 0) { slotColorPickingFinished(KoColor(color, image()->colorSpace())); return; } } } KisPaintDeviceSP device = fromCurrentNode ? currentNode()->colorPickSourceDevice() : image()->projection(); // Used for color picker blending. KoColor currentColor = canvas()->resourceManager()->foregroundColor(); if( pickingJob.action == PickBgNode || pickingJob.action == PickBgImage ){ currentColor = canvas()->resourceManager()->backgroundColor(); } image()->addJob(m_pickerStrokeId, new KisColorPickerStrokeStrategy::Data(device, imagePoint, currentColor)); } void KisToolPaint::beginAlternateAction(KoPointerEvent *event, AlternateAction action) { if (isPickingAction(action)) { KIS_ASSERT_RECOVER_RETURN(!m_pickerStrokeId); setMode(SECONDARY_PAINT_MODE); KisColorPickerStrokeStrategy *strategy = new KisColorPickerStrokeStrategy(); connect(strategy, &KisColorPickerStrokeStrategy::sigColorUpdated, this, &KisToolPaint::slotColorPickingFinished); m_pickerStrokeId = image()->startStroke(strategy); m_colorPickingCompressor->start(PickingJob(event->point, action)); requestUpdateOutline(event->point, event); } else { KisTool::beginAlternateAction(event, action); } } void KisToolPaint::continueAlternateAction(KoPointerEvent *event, AlternateAction action) { if (isPickingAction(action)) { KIS_ASSERT_RECOVER_RETURN(m_pickerStrokeId); m_colorPickingCompressor->start(PickingJob(event->point, action)); requestUpdateOutline(event->point, event); } else { KisTool::continueAlternateAction(event, action); } } void KisToolPaint::endAlternateAction(KoPointerEvent *event, AlternateAction action) { if (isPickingAction(action)) { KIS_ASSERT_RECOVER_RETURN(m_pickerStrokeId); image()->endStroke(m_pickerStrokeId); m_pickerStrokeId.clear(); requestUpdateOutline(event->point, event); setMode(HOVER_MODE); } else { KisTool::endAlternateAction(event, action); } } int KisToolPaint::colorPreviewResourceId(AlternateAction action) { bool toForegroundColor = action == PickFgNode || action == PickFgImage; int resource = toForegroundColor ? KoCanvasResourceManager::ForegroundColor : KoCanvasResourceManager::BackgroundColor; return resource; } void KisToolPaint::slotColorPickingFinished(const KoColor &color) { canvas()->resourceManager()->setResource(m_pickingResource, color); if (!m_showColorPreview) return; KisCanvas2 * kisCanvas = dynamic_cast(canvas()); KIS_ASSERT_RECOVER_RETURN(kisCanvas); QColor previewColor = kisCanvas->displayColorConverter()->toQColor(color); m_colorPreviewShowComparePlate = true; m_colorPreviewCurrentColor = previewColor; requestUpdateOutline(m_outlineDocPoint, 0); } void KisToolPaint::mousePressEvent(KoPointerEvent *event) { KisTool::mousePressEvent(event); if (mode() == KisTool::HOVER_MODE) { requestUpdateOutline(event->point, event); } } void KisToolPaint::mouseMoveEvent(KoPointerEvent *event) { KisTool::mouseMoveEvent(event); if (mode() == KisTool::HOVER_MODE) { requestUpdateOutline(event->point, event); } } void KisToolPaint::mouseReleaseEvent(KoPointerEvent *event) { KisTool::mouseReleaseEvent(event); if (mode() == KisTool::HOVER_MODE) { requestUpdateOutline(event->point, event); } } QWidget * KisToolPaint::createOptionWidget() { QWidget *optionWidget = new QWidget(); optionWidget->setObjectName(toolId()); QVBoxLayout *verticalLayout = new QVBoxLayout(optionWidget); verticalLayout->setObjectName("KisToolPaint::OptionWidget::VerticalLayout"); verticalLayout->setContentsMargins(0,0,0,0); verticalLayout->setSpacing(5); // See https://bugs.kde.org/show_bug.cgi?id=316896 QWidget *specialSpacer = new QWidget(optionWidget); specialSpacer->setObjectName("SpecialSpacer"); specialSpacer->setFixedSize(0, 0); verticalLayout->addWidget(specialSpacer); verticalLayout->addWidget(specialSpacer); m_optionsWidgetLayout = new QGridLayout(); m_optionsWidgetLayout->setColumnStretch(1, 1); verticalLayout->addLayout(m_optionsWidgetLayout); m_optionsWidgetLayout->setContentsMargins(0,0,0,0); m_optionsWidgetLayout->setSpacing(5); if (!quickHelp().isEmpty()) { QPushButton *push = new QPushButton(KisIconUtils::loadIcon("help-contents"), QString(), optionWidget); connect(push, SIGNAL(clicked()), this, SLOT(slotPopupQuickHelp())); QHBoxLayout *hLayout = new QHBoxLayout(optionWidget); hLayout->addWidget(push); hLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Fixed)); verticalLayout->addLayout(hLayout); } return optionWidget; } QWidget* findLabelWidget(QGridLayout *layout, QWidget *control) { QWidget *result = 0; int index = layout->indexOf(control); int row, col, rowSpan, colSpan; layout->getItemPosition(index, &row, &col, &rowSpan, &colSpan); if (col > 0) { QLayoutItem *item = layout->itemAtPosition(row, col - 1); if (item) { result = item->widget(); } } else { QLayoutItem *item = layout->itemAtPosition(row, col + 1); if (item) { result = item->widget(); } } return result; } void KisToolPaint::showControl(QWidget *control, bool value) { control->setVisible(value); QWidget *label = findLabelWidget(m_optionsWidgetLayout, control); if (label) { label->setVisible(value); } } void KisToolPaint::enableControl(QWidget *control, bool value) { control->setEnabled(value); QWidget *label = findLabelWidget(m_optionsWidgetLayout, control); if (label) { label->setEnabled(value); } } void KisToolPaint::addOptionWidgetLayout(QLayout *layout) { Q_ASSERT(m_optionsWidgetLayout != 0); int rowCount = m_optionsWidgetLayout->rowCount(); m_optionsWidgetLayout->addLayout(layout, rowCount, 0, 1, 2); } void KisToolPaint::addOptionWidgetOption(QWidget *control, QWidget *label) { Q_ASSERT(m_optionsWidgetLayout != 0); if (label) { m_optionsWidgetLayout->addWidget(label, m_optionsWidgetLayout->rowCount(), 0); m_optionsWidgetLayout->addWidget(control, m_optionsWidgetLayout->rowCount() - 1, 1); } else { m_optionsWidgetLayout->addWidget(control, m_optionsWidgetLayout->rowCount(), 0, 1, 2); } } void KisToolPaint::setOpacity(qreal opacity) { m_opacity = quint8(opacity * OPACITY_OPAQUE_U8); } const KoCompositeOp* KisToolPaint::compositeOp() { if (currentNode()) { KisPaintDeviceSP device = currentNode()->paintDevice(); if (device) { QString op = canvas()->resourceManager()->resource(KisCanvasResourceProvider::CurrentCompositeOp).toString(); return device->colorSpace()->compositeOp(op); } } return 0; } void KisToolPaint::slotPopupQuickHelp() { QWhatsThis::showText(QCursor::pos(), quickHelp()); } KisToolPaint::NodePaintAbility KisToolPaint::nodePaintAbility() { KisNodeSP node = currentNode(); if (!node) { return NONE; } if (node->inherits("KisShapeLayer")) { return VECTOR; } if (node->inherits("KisCloneLayer")) { return CLONE; } if (node->paintDevice()) { return PAINT; } return NONE; } void KisToolPaint::activatePrimaryAction() { pickColorWasOverridden(); setOutlineEnabled(true); KisTool::activatePrimaryAction(); } void KisToolPaint::deactivatePrimaryAction() { setOutlineEnabled(false); KisTool::deactivatePrimaryAction(); } bool KisToolPaint::isOutlineEnabled() const { return m_isOutlineEnabled; } void KisToolPaint::setOutlineEnabled(bool value) { m_isOutlineEnabled = value; requestUpdateOutline(m_outlineDocPoint, 0); } void KisToolPaint::increaseBrushSize() { qreal paintopSize = currentPaintOpPreset()->settings()->paintOpSize(); std::vector::iterator result = std::upper_bound(m_standardBrushSizes.begin(), m_standardBrushSizes.end(), qRound(paintopSize)); int newValue = result != m_standardBrushSizes.end() ? *result : m_standardBrushSizes.back(); currentPaintOpPreset()->settings()->setPaintOpSize(newValue); requestUpdateOutline(m_outlineDocPoint, 0); } void KisToolPaint::decreaseBrushSize() { qreal paintopSize = currentPaintOpPreset()->settings()->paintOpSize(); std::vector::reverse_iterator result = std::upper_bound(m_standardBrushSizes.rbegin(), m_standardBrushSizes.rend(), (int)paintopSize, std::greater()); int newValue = result != m_standardBrushSizes.rend() ? *result : m_standardBrushSizes.front(); currentPaintOpPreset()->settings()->setPaintOpSize(newValue); requestUpdateOutline(m_outlineDocPoint, 0); } QRectF KisToolPaint::colorPreviewDocRect(const QPointF &outlineDocPoint) { if (!m_showColorPreview) return QRect(); KisConfig cfg; const QRectF colorPreviewViewRect = cfg.colorPreviewRect(); const QRectF colorPreviewDocumentRect = canvas()->viewConverter()->viewToDocument(colorPreviewViewRect); return colorPreviewDocumentRect.translated(outlineDocPoint); } void KisToolPaint::requestUpdateOutline(const QPointF &outlineDocPoint, const KoPointerEvent *event) { if (!m_supportOutline) return; KisConfig cfg; KisPaintOpSettings::OutlineMode outlineMode; if (isOutlineEnabled() && (mode() == KisTool::GESTURE_MODE || ((cfg.newOutlineStyle() == OUTLINE_FULL || cfg.newOutlineStyle() == OUTLINE_CIRCLE || cfg.newOutlineStyle() == OUTLINE_TILT) && ((mode() == HOVER_MODE) || (mode() == PAINT_MODE && cfg.showOutlineWhilePainting()))))) { // lisp forever! outlineMode.isVisible = true; if (cfg.newOutlineStyle() == OUTLINE_CIRCLE) { outlineMode.forceCircle = true; } else if(cfg.newOutlineStyle() == OUTLINE_TILT) { outlineMode.forceCircle = true; outlineMode.showTiltDecoration = true; } else { // noop } } outlineMode.forceFullSize = cfg.forceAlwaysFullSizedOutline(); m_outlineDocPoint = outlineDocPoint; m_currentOutline = getOutlinePath(m_outlineDocPoint, event, outlineMode); QRectF outlinePixelRect = m_currentOutline.boundingRect(); QRectF outlineDocRect = currentImage()->pixelToDocument(outlinePixelRect); // This adjusted call is needed as we paint with a 3 pixel wide brush and the pen is outside the bounds of the path - // Pen uses view coordinates so we have to zoom the document value to match 2 pixel in view coordiates + // Pen uses view coordinates so we have to zoom the document value to match 2 pixel in view coordinates // See BUG 275829 qreal zoomX; qreal zoomY; canvas()->viewConverter()->zoom(&zoomX, &zoomY); qreal xoffset = 2.0/zoomX; qreal yoffset = 2.0/zoomY; if (!outlineDocRect.isEmpty()) { outlineDocRect.adjust(-xoffset,-yoffset,xoffset,yoffset); } QRectF colorPreviewDocRect = this->colorPreviewDocRect(m_outlineDocPoint); QRectF colorPreviewDocUpdateRect; if (!colorPreviewDocRect.isEmpty()) { colorPreviewDocUpdateRect.adjust(-xoffset,-yoffset,xoffset,yoffset); } // DIRTY HACK ALERT: we should fetch the assistant's dirty rect when requesting // the update, instead of just dumbly update the entire canvas! KisCanvas2 * kiscanvas = dynamic_cast(canvas()); KisPaintingAssistantsDecorationSP decoration = kiscanvas->paintingAssistantsDecoration(); if (decoration && decoration->visible()) { kiscanvas->updateCanvas(); } else { // TODO: only this branch should be present! if (!m_oldColorPreviewUpdateRect.isEmpty()) { canvas()->updateCanvas(m_oldColorPreviewUpdateRect); } if (!m_oldOutlineRect.isEmpty()) { canvas()->updateCanvas(m_oldOutlineRect); } if (!outlineDocRect.isEmpty()) { canvas()->updateCanvas(outlineDocRect); } if (!colorPreviewDocUpdateRect.isEmpty()) { canvas()->updateCanvas(colorPreviewDocUpdateRect); } } m_oldOutlineRect = outlineDocRect; m_oldColorPreviewRect = colorPreviewDocRect; m_oldColorPreviewUpdateRect = colorPreviewDocUpdateRect; } QPainterPath KisToolPaint::getOutlinePath(const QPointF &documentPos, const KoPointerEvent *event, KisPaintOpSettings::OutlineMode outlineMode) { Q_UNUSED(event); QPointF imagePos = currentImage()->documentToPixel(documentPos); QPainterPath path = currentPaintOpPreset()->settings()-> brushOutline(KisPaintInformation(imagePos), outlineMode); return path; } diff --git a/libs/ui/widgets/kis_cmb_composite.cc b/libs/ui/widgets/kis_cmb_composite.cc index e53e4e43b3..79dd3127fa 100644 --- a/libs/ui/widgets/kis_cmb_composite.cc +++ b/libs/ui/widgets/kis_cmb_composite.cc @@ -1,482 +1,482 @@ /* * kis_cmb_composite.cc - part of KImageShop/Krayon/Krita * * Copyright (c) 2004 Boudewijn Rempt (boud@valdyas.org) * Copyright (c) 2011 Silvio Heinrich * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_cmb_composite.h" #include #include #include "kis_composite_ops_model.h" #include "kis_categorized_item_delegate.h" #include ////////////////////////////////////////////////////////////////////////////////////////// // ---- KisCompositeOpListWidget ------------------------------------------------------ // KisCompositeOpListWidget::KisCompositeOpListWidget(QWidget* parent): KisCategorizedListView(parent), m_model(new KisSortedCompositeOpListModel(this)) { setModel(m_model); setItemDelegate(new KisCategorizedItemDelegate(this)); } KisCompositeOpListWidget::~KisCompositeOpListWidget() { } KoID KisCompositeOpListWidget::selectedCompositeOp() const { KoID op; if (m_model->entryAt(op, currentIndex())) { return op; } return KoCompositeOpRegistry::instance().getDefaultCompositeOp(); } ////////////////////////////////////////////////////////////////////////////////////////// // ---- KisCompositeOpComboBox -------------------------------------------------------- // KisCompositeOpComboBox::KisCompositeOpComboBox(QWidget* parent): QComboBox(parent), m_model(new KisSortedCompositeOpListModel(this)), m_allowToHidePopup(true) { m_view = new KisCategorizedListView(); m_view->setCompositeBoxControl(true); setMaxVisibleItems(100); setSizeAdjustPolicy(AdjustToContents); m_view->setResizeMode(QListView::Adjust); setToolTip(i18n("Blending Mode")); setModel(m_model); setView(m_view); setItemDelegate(new KisCategorizedItemDelegate(this)); connect(m_view, SIGNAL(sigCategoryToggled(const QModelIndex&, bool)), SLOT(slotCategoryToggled(const QModelIndex&, bool))); connect(m_view, SIGNAL(sigEntryChecked(const QModelIndex&)), SLOT(slotEntryChecked(const QModelIndex&))); selectCompositeOp(KoCompositeOpRegistry::instance().getDefaultCompositeOp()); KisAction *action = 0; // // Cycle through blending modes // // Shift + + (plus) or – (minus) // KisAction *action = new KisAction(i18n("Next Blending Mode"), this); // action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_Plus)); // connect(action, SIGNAL(triggered()), SLOT(slotNextBlendingMode())); // m_actions << action; // action = new KisAction(i18n("Previous Blending Mode"), this); // action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_Minus)); // connect(action, SIGNAL(triggered()), SLOT(slotPreviousBlendingMode())); // m_actions << action; // Normal // Shift + Alt + N action = new KisAction(i18n("Select Normal Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_N)); connect(action, SIGNAL(triggered()), SLOT(slotNormal())); m_actions << action; // Dissolve // Shift + Alt + I action = new KisAction(i18n("Select Dissolve Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_I)); connect(action, SIGNAL(triggered()), SLOT(slotDissolve())); m_actions << action; // Behind (Brush tool only) // Shift + Alt + Q action = new KisAction(i18n("Select Behind Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_Q)); connect(action, SIGNAL(triggered()), SLOT(slotBehind())); m_actions << action; // Clear (Brush tool only) // Shift + Alt + R action = new KisAction(i18n("Select Clear Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_R)); connect(action, SIGNAL(triggered()), SLOT(slotClear())); m_actions << action; // Darken // Shift + Alt + K action = new KisAction(i18n("Select Darken Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_K)); connect(action, SIGNAL(triggered()), SLOT(slotDarken())); m_actions << action; // Multiply // Shift + Alt + M action = new KisAction(i18n("Select Multiply Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_M)); connect(action, SIGNAL(triggered()), SLOT(slotMultiply())); m_actions << action; // Color Burn // Shift + Alt + B action = new KisAction(i18n("Select Color Burn Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_B)); connect(action, SIGNAL(triggered()), SLOT(slotColorBurn())); m_actions << action; // Linear Burn // Shift + Alt + A action = new KisAction(i18n("Select Linear Burn Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_A)); connect(action, SIGNAL(triggered()), SLOT(slotLinearBurn())); m_actions << action; // Lighten // Shift + Alt + G action = new KisAction(i18n("Select Lighten Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_G)); connect(action, SIGNAL(triggered()), SLOT(slotLighten())); m_actions << action; // Screen // Shift + Alt + S action = new KisAction(i18n("Select Screen Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_S)); connect(action, SIGNAL(triggered()), SLOT(slotScreen())); m_actions << action; // Color Dodge // Shift + Alt + D action = new KisAction(i18n("Select Color Dodge Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_D)); connect(action, SIGNAL(triggered()), SLOT(slotColorDodge())); m_actions << action; // Linear Dodge // Shift + Alt + W action = new KisAction(i18n("Select Linear Dodge Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_W)); connect(action, SIGNAL(triggered()), SLOT(slotLinearDodge())); m_actions << action; // Overlay // Shift + Alt + O action = new KisAction(i18n("Select Overlay Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_O)); connect(action, SIGNAL(triggered()), SLOT(slotOverlay())); m_actions << action; // Hard Overlay // Shift + Alt + P action = new KisAction(i18n("Select Hard Overlay Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_P)); connect(action, SIGNAL(triggered()), SLOT(slotHardOverlay())); m_actions << action; // Soft Light // Shift + Alt + F action = new KisAction(i18n("Select Soft Light Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_F)); connect(action, SIGNAL(triggered()), SLOT(slotSoftLight())); m_actions << action; // Hard Light // Shift + Alt + H action = new KisAction(i18n("Select Hard Light Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_H)); connect(action, SIGNAL(triggered()), SLOT(slotHardLight())); m_actions << action; // Vivid Light // Shift + Alt + V action = new KisAction(i18n("Select Vivid Light Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_V)); connect(action, SIGNAL(triggered()), SLOT(slotVividLight())); m_actions << action; // Linear Light // Shift + Alt + J action = new KisAction(i18n("Select Linear Light Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_J)); connect(action, SIGNAL(triggered()), SLOT(slotLinearLight())); m_actions << action; // Pin Light // Shift + Alt + Z action = new KisAction(i18n("Select Pin Light Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_Z)); connect(action, SIGNAL(triggered()), SLOT(slotPinLight())); m_actions << action; // Hard Mix // Shift + Alt + L action = new KisAction(i18n("Select Hard Mix Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_L)); connect(action, SIGNAL(triggered()), SLOT(slotHardMix())); m_actions << action; // Difference // Shift + Alt + E action = new KisAction(i18n("Select Difference Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_E)); connect(action, SIGNAL(triggered()), SLOT(slotDifference())); m_actions << action; // Exclusion // Shift + Alt + X action = new KisAction(i18n("Select Exclusion Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_X)); connect(action, SIGNAL(triggered()), SLOT(slotExclusion())); m_actions << action; // Hue // Shift + Alt + U action = new KisAction(i18n("Select Hue Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_U)); connect(action, SIGNAL(triggered()), SLOT(slotHue())); m_actions << action; // Saturation // Shift + Alt + T action = new KisAction(i18n("Select Saturation Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_T)); connect(action, SIGNAL(triggered()), SLOT(slotSaturation())); m_actions << action; // Color // Shift + Alt + C action = new KisAction(i18n("Select Color Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_C)); connect(action, SIGNAL(triggered()), SLOT(slotColor())); m_actions << action; // Luminosity // Shift + Alt + Y action = new KisAction(i18n("Select Luminosity Blending Mode"), this); action->setDefaultShortcut(QKeySequence(Qt::SHIFT + Qt::ALT + Qt::Key_Y)); connect(action, SIGNAL(triggered()), SLOT(slotLuminosity())); m_actions << action; } KisCompositeOpComboBox::~KisCompositeOpComboBox() { delete m_view; } void KisCompositeOpComboBox::validate(const KoColorSpace *cs) { m_model->validate(cs); } void KisCompositeOpComboBox::selectCompositeOp(const KoID &op) { QModelIndex index = m_model->indexOf(op); setCurrentIndex(index.row()); } KoID KisCompositeOpComboBox::selectedCompositeOp() const { KoID op; if (m_model->entryAt(op, m_model->index(currentIndex(), 0))) { return op; } return KoCompositeOpRegistry::instance().getDefaultCompositeOp(); } QList KisCompositeOpComboBox::blendmodeActions() const { return m_actions; } void KisCompositeOpComboBox::slotCategoryToggled(const QModelIndex& index, bool toggled) { Q_UNUSED(index); Q_UNUSED(toggled); //NOTE: this will (should) fit the size of the // popup widget to the view // don't know if this is expected behaviour // on all supported platforms. - // Thre is nothing written about this in the docs. + // There is nothing written about this in the docs. showPopup(); } void KisCompositeOpComboBox::slotEntryChecked(const QModelIndex& index) { Q_UNUSED(index); m_allowToHidePopup = false; } void KisCompositeOpComboBox::hidePopup() { if (m_allowToHidePopup) { QComboBox::hidePopup(); } else { QComboBox::showPopup(); } m_allowToHidePopup = true; } void KisCompositeOpComboBox::slotNextBlendingMode() { if (currentIndex() < count()) { setCurrentIndex(currentIndex() + 1); } } void KisCompositeOpComboBox::slotPreviousBlendingMode() { if (currentIndex() > 0) { setCurrentIndex(currentIndex() - 1); } } void KisCompositeOpComboBox::slotNormal() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_OVER)); } void KisCompositeOpComboBox::slotDissolve() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_DISSOLVE)); } void KisCompositeOpComboBox::slotBehind() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_BEHIND)); } void KisCompositeOpComboBox::slotClear() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_CLEAR)); } void KisCompositeOpComboBox::slotDarken() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_DARKEN)); } void KisCompositeOpComboBox::slotMultiply() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_MULT)); } void KisCompositeOpComboBox::slotColorBurn() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_BURN)); } void KisCompositeOpComboBox::slotLinearBurn() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_LINEAR_BURN)); } void KisCompositeOpComboBox::slotLighten() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_LIGHTEN)); } void KisCompositeOpComboBox::slotScreen() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_SCREEN)); } void KisCompositeOpComboBox::slotColorDodge() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_DODGE)); } void KisCompositeOpComboBox::slotLinearDodge() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_LINEAR_DODGE)); } void KisCompositeOpComboBox::slotOverlay() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_OVERLAY)); } void KisCompositeOpComboBox::slotHardOverlay() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_HARD_OVERLAY)); } void KisCompositeOpComboBox::slotSoftLight() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_SOFT_LIGHT_PHOTOSHOP)); } void KisCompositeOpComboBox::slotHardLight() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_HARD_LIGHT)); } void KisCompositeOpComboBox::slotVividLight() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_VIVID_LIGHT)); } void KisCompositeOpComboBox::slotLinearLight() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_LINEAR_LIGHT)); } void KisCompositeOpComboBox::slotPinLight() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_PIN_LIGHT)); } void KisCompositeOpComboBox::slotHardMix() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_HARD_MIX_PHOTOSHOP)); } void KisCompositeOpComboBox::slotDifference() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_DIFF)); } void KisCompositeOpComboBox::slotExclusion() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_EXCLUSION)); } void KisCompositeOpComboBox::slotHue() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_HUE)); } void KisCompositeOpComboBox::slotSaturation() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_SATURATION)); } void KisCompositeOpComboBox::slotColor() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_COLOR)); } void KisCompositeOpComboBox::slotLuminosity() { selectCompositeOp(KoCompositeOpRegistry::instance().getKoID(COMPOSITE_LUMINIZE)); } diff --git a/libs/ui/widgets/kis_preset_live_preview_view.h b/libs/ui/widgets/kis_preset_live_preview_view.h index a09d1c366d..4135851cfc 100644 --- a/libs/ui/widgets/kis_preset_live_preview_view.h +++ b/libs/ui/widgets/kis_preset_live_preview_view.h @@ -1,150 +1,150 @@ /* * Copyright (c) 2017 Scott Petrovic * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef _KIS_PRESET_LIVE_PREVIEW_ #define _KIS_PRESET_LIVE_PREVIEW_ #include #include #include #include #include #include "kis_paintop_preset.h" #include "KoColorSpaceRegistry.h" #include "kis_paint_layer.h" #include "kis_painter.h" #include "kis_distance_information.h" #include "kis_painting_information_builder.h" #include #include #include /** * Widget for displaying a live brush preview of your * selected brush. It listens for signalsetting changes * that the brush preset outputs and updates the preview * accordingly. This class can be added to a UI file * similar to how a QGraphicsView is added */ class KisPresetLivePreviewView : public QGraphicsView { Q_OBJECT public: KisPresetLivePreviewView(QWidget *parent); ~KisPresetLivePreviewView(); /** * @brief one time setup for initialization of many variables. * This live preview might be in a UI file, so make sure to * call this before starting to use it */ void setup(); /** * @brief set the current preset from resource manager for the live preview to use. * Good to call this every stroke update in case the preset has changed * @param the current preset from the resource manager */ void setCurrentPreset(KisPaintOpPresetSP preset); void updateStroke(); private: /// internally sets the image area for brush preview KisImageSP m_image; /// internally sets the layer area for brush preview KisLayerSP m_layer; /// internally sets the color space for brush preview const KoColorSpace *m_colorSpace; /// the color which is used for rendering the stroke KoColor m_paintColor; /// the scene that can add items like text and the brush stroke image QGraphicsScene *m_brushPreviewScene; /// holds the preview brush stroke data QGraphicsPixmapItem *m_sceneImageItem; /// holds the 'no preview available' text object QGraphicsTextItem *m_noPreviewText; /// holds the width and height of the image of the brush preview /// Probably can later add set functions to make this customizable /// It is hard-coded to 1200 x 400 for right now for image size QRect m_canvasSize; /// convenience variable used internally when positioning the objects /// and points in the scene QPointF m_canvasCenterPoint; /// internal variables for constructing the stroke start and end shape /// there are two points that construct the "S" curve with this KisDistanceInformation m_currentDistance; QPainterPath m_curvedLine; KisPaintInformation m_curvePointPI1; KisPaintInformation m_curvePointPI2; /// internally stores the current preset. /// See setCurrentPreset(KisPaintOpPresetSP preset) /// for setting this externally KisPaintOpPresetSP m_currentPreset; /// holds the current zoom(scale) level of scene float m_scaleFactor; /// internal reference for internal brush size /// used to check if our brush size has changed - /// do zooming and other things internall if it has changed + /// do zooming and other things internally if it has changed float m_currentBrushSize = 1.0; /// the range of brush sizes that will control zooming in/out const float m_minBrushVal = 10.0; const float m_maxBrushVal = 100.0; /// range of scale values. 1.0 == 100% const qreal m_minScale = 1.0; const qreal m_maxScale = 0.3; /// multiplier that is used for lengthening the brush stroke points const float m_minStrokeScale = 0.4; // for smaller brush stroke const float m_maxStrokeScale = 1.0; // for larger brush stroke /** * @brief works as both clearing the previous stroke, providing * striped backgrounds for smudging brushes, and text if there is no preview */ void paintBackground(); /** * @brief creates and performs the actual stroke that goes on top of the background * this is internally and should always be called after the paintBackground() */ void setupAndPaintStroke(); }; #endif diff --git a/libs/widgets/KoUnitDoubleSpinBox.h b/libs/widgets/KoUnitDoubleSpinBox.h index 4271456678..b889eee867 100644 --- a/libs/widgets/KoUnitDoubleSpinBox.h +++ b/libs/widgets/KoUnitDoubleSpinBox.h @@ -1,119 +1,119 @@ /* This file is part of the KDE project Copyright (C) 2002, Rob Buis(buis@kde.org) Copyright (C) 2004, Nicolas GOUTTE Copyright (C) 2007, Thomas Zander This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOUNITDOUBLESPINBOX_H #define KOUNITDOUBLESPINBOX_H #include "kritawidgets_export.h" #include class KoUnit; /** * Spin box for double precision numbers with unit display. * Use this widget for any value that represents a real measurable value for consistency throughout * Krita. - * This widget shows the value in the user-selected units (inch, milimeters, etc) but keeps the + * This widget shows the value in the user-selected units (inch, millimeters, etc) but keeps the * Krita-widget default measurement unit internally. This has the advantage that just setting and * getting a value will not change the value due to conversions. * The KoDocument class has a unit() method for consistent (document wide) configuration of the * used unit. * It is advised to use a QDoubleSpinBox in QtDesigner and then use the context-menu item: 'Promote to Custom Widget' and use the values: 'classname=KoUnitDoubleSpinBox', 'headerfile=KoUnitDoubleSpinBox.h' * This will generate code that uses this spinbox in the correct manner. * * This class need to be replaced as much as possible with \see KisDoubleParseUnitSpinBox to add math parsing ability. */ class KRITAWIDGETS_EXPORT KoUnitDoubleSpinBox : public QDoubleSpinBox { Q_OBJECT public: /** * Constructor * Create a new spinBox with very broad range predefined. * This spinbox will have min and max borders of 10000 points and use * the default unit of points. * @param parent the parent widget */ explicit KoUnitDoubleSpinBox( QWidget *parent = 0); ~KoUnitDoubleSpinBox() override; /** * Set the new value in points which will then be converted to the current unit for display * @param newValue the new value * @see value() */ virtual void changeValue( double newValue ); /** * This spinbox shows the internal value after a conversion to the unit set here. */ virtual void setUnit( const KoUnit &); /// @return the current value, converted in points double value( ) const; /// Set minimum value in points. void setMinimum(double min); /// Set maximum value in points. void setMaximum(double max); /// Set step size in the current unit. void setLineStep(double step); /// Set step size in points. void setLineStepPt(double step); /// Set minimum, maximum value and the step size (all in points) void setMinMaxStep( double min, double max, double step ); /// reimplemented from superclass, will forward to KoUnitDoubleValidator QValidator::State validate(QString &input, int &pos) const override; /** * Transform the double in a nice text, using locale symbols * @param value the number as double * @return the resulting string */ QString textFromValue( double value ) const override; /** * Transform a string into a double, while taking care of locale specific symbols. * @param str the string to transform into a number * @return the value as double */ double valueFromText( const QString& str ) const override; Q_SIGNALS: /// emitted like valueChanged in the parent, but this one emits the point value void valueChangedPt( qreal ); private: class Private; Private * const d; private Q_SLOTS: // exists to do emits for valueChangedPt void privateValueChanged(); }; #endif diff --git a/libs/widgetutils/xmlgui/khelpmenu.h b/libs/widgetutils/xmlgui/khelpmenu.h index f66792ebd6..39a93f8723 100644 --- a/libs/widgetutils/xmlgui/khelpmenu.h +++ b/libs/widgetutils/xmlgui/khelpmenu.h @@ -1,267 +1,267 @@ /* * This file is part of the KDE Libraries * Copyright (C) 1999-2000 Espen Sand (espen@kde.org) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * */ #ifndef KHELPMENU_H #define KHELPMENU_H #include #include #include class QMenu; class QWidget; class QAction; class KAboutData; class KHelpMenuPrivate; /** * @short Standard %KDE help menu with dialog boxes. * * This class provides the standard %KDE help menu with the default "about" * dialog boxes and help entry. * * This class is used in KMainWindow so * normally you don't need to use this class yourself. However, if you * need the help menu or any of its dialog boxes in your code that is * not subclassed from KMainWindow you should use this class. * * The usage is simple: * * \code * mHelpMenu = new KHelpMenu( this, ); * kmenubar->addMenu(mHelpMenu->menu() ); * \endcode * * or if you just want to open a dialog box: * * \code * mHelpMenu = new KHelpMenu( this, ); * connect( this, SIGNAL(someSignal()), mHelpMenu,SLOT(aboutKDE())); * \endcode * * IMPORTANT: * The first time you use KHelpMenu::menu(), a QMenu object is * allocated. Only one object is created by the class so if you call * KHelpMenu::menu() twice or more, the same pointer is returned. The class * will destroy the popupmenu in the destructor so do not delete this * pointer yourself. * * The KHelpMenu object will be deleted when its parent is destroyed but you * can delete it yourself if you want. The code below will always work. * * \code * MyClass::~MyClass() * { * delete mHelpMenu; * } * \endcode * * * Using your own "about application" dialog box: * * The standard "about application" dialog box is quite simple. If you * need a dialog box with more functionality you must design that one * yourself. When you want to display the dialog, you simply need to * connect the help menu signal showAboutApplication() to your slot. * * \code * void MyClass::myFunc() * { * .. * KHelpMenu *helpMenu = new KHelpMenu( this ); * connect( helpMenu, SIGNAL(showAboutApplication()), * this, SLOT(myDialogSlot())); * .. * } * * void MyClass::myDialogSlot() * { * * } * \endcode * * \image html khelpmenu.png "KDE Help Menu" * * KHelpMenu respects Kiosk settings (see the KAuthorized namespace in the * KConfig framework). In particular, system administrators can disable items * on this menu using some subset of the following configuration: * @verbatim [KDE Action Restrictions][$i] actions/help_contents=false actions/help_whats_this=false actions/help_report_bug=false actions/switch_application_language=false actions/help_about_app=false actions/help_about_kde=false @endverbatim * * @author Espen Sand (espen@kde.org) */ class KRITAWIDGETUTILS_EXPORT KHelpMenu : public QObject { Q_OBJECT public: /** * Constructor. * * @param parent The parent of the dialog boxes. The boxes are modeless * and will be centered with respect to the parent. * @param aboutAppText User definable string that is used in the * default application dialog box. * @param showWhatsThis Decides whether a "Whats this" entry will be * added to the dialog. * */ explicit KHelpMenu(QWidget *parent = 0, const QString &aboutAppText = QString(), bool showWhatsThis = true); /** * Constructor. * * This alternative constructor is mainly useful if you want to * override the standard actions (aboutApplication(), aboutKDE(), * helpContents(), reportBug, and optionally whatsThis). * * @param parent The parent of the dialog boxes. The boxes are modeless * and will be centered with respect to the parent. * @param aboutData User and app data used in the About app dialog * @param showWhatsThis Decides whether a "Whats this" entry will be * added to the dialog. */ KHelpMenu(QWidget *parent, const KAboutData &aboutData, bool showWhatsThis = true); /** * Destructor * * Destroys dialogs and the menu pointer retuned by menu */ ~KHelpMenu() override; /** * Returns a popup menu you can use in the menu bar or where you * need it. * * The returned menu is configured with an icon, a title and * menu entries. Therefore adding the returned pointer to your menu - * is enougth to have access to the help menu. + * is enough to have access to the help menu. * * Note: This method will only create one instance of the menu. If * you call this method twice or more the same pointer is returned. */ QMenu *menu(); enum MenuId { menuHelpContents = 0, menuWhatsThis = 1, menuAboutApp = 2, menuAboutKDE = 3, menuReportBug = 4, menuSwitchLanguage = 5 }; /** * Returns the QAction * associated with the given parameter * Will return 0 pointers if menu() has not been called * * @param id The id of the action of which you want to get QAction * */ QAction *action(MenuId id) const; public Q_SLOTS: /** * Opens the help page for the application. The application name is * used as a key to determine what to display and the system will attempt * to open \/index.html. */ void appHelpActivated(); /** * Activates What's This help for the application. */ void contextHelpActivated(); /** * Opens an application specific dialog box. * * The method will try to open the about box using the following steps: * - If the showAboutApplication() signal is connected, then it will be called. * This means there is an application defined aboutBox. * - If the aboutData was set in the constructor a KAboutApplicationDialog will be created. * - Else a default about box using the aboutAppText from the constructor will be created. */ void aboutApplication(); /** * Opens the standard "About KDE" dialog box. */ void aboutKDE(); /** * Opens the standard "Report Bugs" dialog box. */ void reportBug(); /** * Opens the changing default application language dialog box. */ void switchApplicationLanguage(); private Q_SLOTS: /** * Connected to the menu pointer (if created) to detect a delete * operation on the pointer. You should not delete the pointer in your * code yourself. Let the KHelpMenu destructor do the job. */ void menuDestroyed(); /** * Connected to the dialogs (about kde and bug report) to detect * when they are finished. */ void dialogFinished(); /** * This slot will delete a dialog (about kde or bug report) if the * dialog pointer is not zero and the dialog is not visible. This * slot is activated by a one shot timer started in dialogHidden */ void timerExpired(); Q_SIGNALS: /** * This signal is emitted from aboutApplication() if no * "about application" string has been defined. The standard * application specific dialog box that is normally activated in * aboutApplication() will not be displayed when this signal * is emitted. */ void showAboutApplication(); private: KHelpMenuPrivate *const d; }; #endif diff --git a/plugins/dockers/advancedcolorselector/kis_color_selector.cpp b/plugins/dockers/advancedcolorselector/kis_color_selector.cpp index 94340e53c7..4f340d21ce 100644 --- a/plugins/dockers/advancedcolorselector/kis_color_selector.cpp +++ b/plugins/dockers/advancedcolorselector/kis_color_selector.cpp @@ -1,366 +1,366 @@ /* * Copyright (c) 2010 Adam Celarek * * This library is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; version 2.1 of the License. * * This library 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_color_selector.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_color_selector_ring.h" #include "kis_color_selector_triangle.h" #include "kis_color_selector_simple.h" #include "kis_color_selector_wheel.h" #include "kis_color_selector_container.h" #include "kis_canvas2.h" #include "kis_signal_compressor.h" #include "KisViewManager.h" KisColorSelector::KisColorSelector(KisColorSelectorConfiguration conf, QWidget* parent) : KisColorSelectorBase(parent), m_ring(0), m_triangle(0), m_slider(0), m_square(0), m_wheel(0), m_mainComponent(0), m_subComponent(0), m_grabbingComponent(0), m_blipDisplay(true) { init(); updateSettings(); setConfiguration(conf); } KisColorSelector::KisColorSelector(QWidget* parent) : KisColorSelectorBase(parent), m_ring(0), m_triangle(0), m_slider(0), m_square(0), m_wheel(0), m_button(0), m_mainComponent(0), m_subComponent(0), m_grabbingComponent(0), m_blipDisplay(true) { init(); updateSettings(); } KisColorSelectorBase* KisColorSelector::createPopup() const { KisColorSelectorBase* popup = new KisColorSelector(0); popup->setColor(m_lastRealColor); return popup; } void KisColorSelector::setConfiguration(KisColorSelectorConfiguration conf) { m_configuration = conf; if(m_mainComponent!=0) { Q_ASSERT(m_subComponent!=0); m_mainComponent->setGeometry(0, 0, 0, 0); m_subComponent->setGeometry(0, 0, 0, 0); m_mainComponent->disconnect(); m_subComponent->disconnect(); } switch (m_configuration.mainType) { case KisColorSelectorConfiguration::Square: m_mainComponent=m_square; break; case KisColorSelectorConfiguration::Wheel: m_mainComponent=m_wheel; break; case KisColorSelectorConfiguration::Triangle: m_mainComponent=m_triangle; break; default: Q_ASSERT(false); } switch (m_configuration.subType) { case KisColorSelectorConfiguration::Ring: m_subComponent=m_ring; break; case KisColorSelectorConfiguration::Slider: m_subComponent=m_slider; break; default: Q_ASSERT(false); } connect(m_mainComponent, SIGNAL(paramChanged(qreal,qreal,qreal,qreal,qreal,qreal,qreal,qreal,qreal)), m_subComponent, SLOT(setParam(qreal,qreal,qreal,qreal,qreal,qreal,qreal,qreal,qreal)), Qt::UniqueConnection); connect(m_subComponent, SIGNAL(paramChanged(qreal,qreal,qreal,qreal,qreal,qreal,qreal,qreal,qreal)), m_mainComponent, SLOT(setParam(qreal,qreal,qreal,qreal, qreal, qreal, qreal, qreal, qreal)), Qt::UniqueConnection); connect(m_mainComponent, SIGNAL(update()), m_signalCompressor, SLOT(start()), Qt::UniqueConnection); connect(m_subComponent, SIGNAL(update()), m_signalCompressor, SLOT(start()), Qt::UniqueConnection); m_mainComponent->setConfiguration(m_configuration.mainTypeParameter, m_configuration.mainType); m_subComponent->setConfiguration(m_configuration.subTypeParameter, m_configuration.subType); QResizeEvent event(QSize(width(), height()), QSize()); resizeEvent(&event); } KisColorSelectorConfiguration KisColorSelector::configuration() const { return m_configuration; } void KisColorSelector::updateSettings() { KisColorSelectorBase::updateSettings(); KConfigGroup cfg = KSharedConfig::openConfig()->group("advancedColorSelector"); setConfiguration(KisColorSelectorConfiguration::fromString(cfg.readEntry("colorSelectorConfiguration", KisColorSelectorConfiguration().toString()))); } void KisColorSelector::updateIcons() { if (m_button) { m_button->setIcon(KisIconUtils::loadIcon("configure")); } } void KisColorSelector::hasAtLeastOneDocument(bool value) { m_hasAtLeastOneDocumentOpen = value; } void KisColorSelector::reset() { KisColorSelectorBase::reset(); if (m_mainComponent) { m_mainComponent->setDirty(); } if (m_subComponent) { m_subComponent->setDirty(); } } void KisColorSelector::paintEvent(QPaintEvent* e) { Q_UNUSED(e); QPainter p(this); p.fillRect(0,0,width(), height(), QColor(128,128,128)); p.setRenderHint(QPainter::Antialiasing); // this variable name isn't entirely accurate to what always happens. see definition in header file to understand it better if (!m_hasAtLeastOneDocumentOpen) { p.setOpacity(0.2); } m_mainComponent->paintEvent(&p); m_subComponent->paintEvent(&p); p.setOpacity(1.0); } inline int iconSize(qreal width, qreal height) { qreal radius = qMin(width, height)/2.; qreal xm = width/2.; qreal ym = height/2.; if(xm>=2*ym || ym>=2*xm) return qBound(5., radius, 32.); qreal a=-2; qreal b=2.*(xm+ym); qreal c=radius*radius-xm*xm-ym*ym; return qBound(5., ((-b+sqrt(b*b-4*a*c))/(2*a)), 32.); } void KisColorSelector::resizeEvent(QResizeEvent* e) { if (m_configuration.subType == KisColorSelectorConfiguration::Ring) { m_ring->setGeometry(0,0,width(), height()); if (displaySettingsButton()) { int size = iconSize(width(), height()); m_button->setGeometry(0, 0, size, size); } if (m_configuration.mainType == KisColorSelectorConfiguration::Triangle) { m_triangle->setGeometry(width()/2-m_ring->innerRadius(), height()/2-m_ring->innerRadius(), m_ring->innerRadius()*2, m_ring->innerRadius()*2); } else { int size = m_ring->innerRadius()*2/sqrt(2.); m_square->setGeometry(width()/2-size/2, height()/2-size/2, size, size); } } else { // type wheel and square if (m_configuration.mainType == KisColorSelectorConfiguration::Wheel) { if(displaySettingsButton()) { int size = iconSize(width(), height()*0.9); m_button->setGeometry(0, height()*0.1, size, size); } m_mainComponent->setGeometry(0, height()*0.1, width(), height()*0.9); m_subComponent->setGeometry( 0, 0, width(), height()*0.1); } else { int buttonSize = 0; if(displaySettingsButton()) { buttonSize = qBound(20, int(0.1*height()), 32); m_button->setGeometry(0, 0, buttonSize, buttonSize); } if(height()>width()) { int selectorHeight=height()-buttonSize; m_mainComponent->setGeometry(0, buttonSize+selectorHeight*0.1, width(), selectorHeight*0.9); m_subComponent->setGeometry( 0, buttonSize, width(), selectorHeight*0.1); } else { int selectorWidth=width()-buttonSize; m_mainComponent->setGeometry(buttonSize, height()*0.1, selectorWidth, height()*0.9); m_subComponent->setGeometry( buttonSize, 0, selectorWidth, height()*0.1); } } } - // reset the currect color after resizing the widget + // reset the correct color after resizing the widget setColor(m_lastRealColor); KisColorSelectorBase::resizeEvent(e); } void KisColorSelector::mousePressEvent(QMouseEvent* e) { e->setAccepted(false); KisColorSelectorBase::mousePressEvent(e); if(!e->isAccepted()) { if(m_mainComponent->wantsGrab(e->x(), e->y())) m_grabbingComponent=m_mainComponent; else if(m_subComponent->wantsGrab(e->x(), e->y())) m_grabbingComponent=m_subComponent; mouseEvent(e); updatePreviousColorPreview(); e->accept(); } } void KisColorSelector::mouseMoveEvent(QMouseEvent* e) { KisColorSelectorBase::mouseMoveEvent(e); mouseEvent(e); e->accept(); } void KisColorSelector::mouseReleaseEvent(QMouseEvent* e) { e->setAccepted(false); KisColorSelectorBase::mousePressEvent(e); if(!e->isAccepted() && !(m_lastRealColor == m_currentRealColor)) { m_lastRealColor = m_currentRealColor; m_lastColorRole = Acs::buttonToRole(e->button()); updateColor(m_lastRealColor, m_lastColorRole, false); updateBaseColorPreview(m_currentRealColor); e->accept(); } m_grabbingComponent=0; } bool KisColorSelector::displaySettingsButton() { return dynamic_cast(parent()); } void KisColorSelector::setColor(const KoColor &color) { m_mainComponent->setColor(color); m_subComponent->setColor(color); m_lastRealColor = color; m_signalCompressor->start(); } void KisColorSelector::mouseEvent(QMouseEvent *e) { if (m_grabbingComponent && (e->buttons() & Qt::LeftButton || e->buttons() & Qt::RightButton)) { m_grabbingComponent->mouseEvent(e->x(), e->y()); KoColor color = m_mainComponent->currentColor(); Acs::ColorRole role = Acs::buttonsToRole(e->button(), e->buttons()); m_currentRealColor = color; requestUpdateColorAndPreview(color, role); } } void KisColorSelector::init() { setAcceptDrops(true); m_lastColorRole = Acs::Foreground; m_ring = new KisColorSelectorRing(this); m_triangle = new KisColorSelectorTriangle(this); m_slider = new KisColorSelectorSimple(this); m_square = new KisColorSelectorSimple(this); m_wheel = new KisColorSelectorWheel(this); if(displaySettingsButton()) { m_button = new QPushButton(this); m_button->setIcon(KisIconUtils::loadIcon("configure")); m_button->setFlat(true); connect(m_button, SIGNAL(clicked()), SIGNAL(settingsButtonClicked())); } // a tablet can send many more signals, than a mouse // this causes many repaints, if updating after every signal. m_signalCompressor = new KisSignalCompressor(20, KisSignalCompressor::FIRST_INACTIVE, this); connect(m_signalCompressor, SIGNAL(timeout()), SLOT(update())); setMinimumSize(40, 40); } diff --git a/plugins/extensions/pykrita/plugin/version_checker.h b/plugins/extensions/pykrita/plugin/version_checker.h index eab0c90ac4..a820838499 100644 --- a/plugins/extensions/pykrita/plugin/version_checker.h +++ b/plugins/extensions/pykrita/plugin/version_checker.h @@ -1,332 +1,332 @@ // This file is part of PyKrita, Krita' Python scripting plugin. // // Copyright (C) 2013 Alex Turbov // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Library General Public // License as published by the Free Software Foundation; either // version 2 of the License, or (at your option) version 3. // // This library 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 // Library General Public License for more details. // // You should have received a copy of the GNU Library General Public License // along with this library; see the file COPYING.LIB. If not, write to // the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, // Boston, MA 02110-1301, USA. #ifndef __VERSION_CHECKER_H__ # define __VERSION_CHECKER_H__ #include #include "utilities.h" # include # include # include namespace PyKrita { /** * \brief Class \c version */ class version { enum type { undefined = -1, zero = 0 }; public: /// Default constructor explicit version(const int _major = zero, const int _minor = zero, const int patch = zero) : m_major(_major) , m_minor(_minor) , m_patch(patch) { } int _major() const { return m_major; } int _minor() const { return m_minor; } int patch() const { return m_patch; } bool isValid() const { return _major() != undefined && _minor() != undefined && patch() != undefined; } operator QString() const { return QString("%1.%2.%3").arg(_major()).arg(_minor()).arg(patch()); } static version fromString(const QString& version_str) { int tmp[3] = {zero, zero, zero}; QStringList parts = version_str.split('.'); for ( unsigned long i = 0 ; i < qMin(static_cast(sizeof(tmp) / sizeof(int)), static_cast(parts.size())) ; ++i ) { bool ok; const int num = parts[i].toInt(&ok); if (ok) tmp[i] = num; else { tmp[i] = undefined; break; } } return version(tmp[0], tmp[1], tmp[2]); }; static version fromPythonObject(PyObject* version_obj) { version v = tryObtainVersionFromTuple(version_obj); if (!v.isValid()) { // PEP396 requires __version__ to be a tuple of integers, // but some modules use a string instead. v = tryObtainVersionFromString(version_obj); } return v; } static version invalid() { static version s_bad(undefined, undefined, undefined); return s_bad; } private: int m_major; int m_minor; int m_patch; static version tryObtainVersionFromTuple(PyObject* version_obj) { Q_ASSERT("Sanity check" && version_obj); if (PyTuple_Check(version_obj) == 0) return version::invalid(); int version_info[3] = {0, 0, 0}; for (unsigned i = 0; i < PyTuple_Size(version_obj); ++i) { PyObject* v = PyTuple_GetItem(version_obj, i); if (v && PyLong_Check(v)) version_info[i] = PyLong_AsLong(v); else version_info[i] = -1; } if (version_info[0] != -1 && version_info[1] != -1 && version_info[2] != -1) return ::PyKrita::version(version_info[0], version_info[1], version_info[2]); return version::invalid(); } /** * Try to parse version string as a simple triplet X.Y.Z. * * \todo Some modules has letters in a version string... * For example current \c pytz version is \e "2013d". */ static version tryObtainVersionFromString(PyObject* version_obj) { Q_ASSERT("Sanity check" && version_obj); if (!Python::isUnicode(version_obj)) return version::invalid(); QString version_str = Python::unicode(version_obj); if (version_str.isEmpty()) return version::invalid(); return version::fromString(version_str); } }; inline bool operator==(const version& left, const version& right) { return left._major() == right._major() && left._minor() == right._minor() && left.patch() == right.patch() ; } inline bool operator!=(const version& left, const version& right) { return !(left == right); } inline bool operator<(const version& left, const version& right) { return left._major() < right._major() || (left._major() == right._major() && left._minor() < right._minor()) || (left._major() == right._major() && left._minor() == right._minor() && left.patch() < right.patch()) ; } inline bool operator>(const version& left, const version& right) { return left._major() > right._major() || (left._major() == right._major() && left._minor() > right._minor()) || (left._major() == right._major() && left._minor() == right._minor() && left.patch() > right.patch()) ; } inline bool operator<=(const version& left, const version& right) { return left == right || left < right; } inline bool operator>=(const version& left, const version& right) { return left == right || left > right; } /** * \brief Class \c version_checker */ class version_checker { public: enum operation { invalid , undefined , less , less_or_equal - , greather - , greather_or_equal + , greater + , greater_or_equal , not_equal , equal , last__ }; /// Default constructor explicit version_checker(const operation op = invalid) : m_op(op) { } bool isValid() const { return m_op != invalid; } bool isEmpty() const { return m_op == undefined; } void bind_second(const version& rhs) { m_rhs = rhs; } bool operator()(const version& left) { switch (m_op) { case less: return left < m_rhs; - case greather: + case greater: return left > m_rhs; case equal: return left == m_rhs; case not_equal: return left != m_rhs; case less_or_equal: return left <= m_rhs; - case greather_or_equal: + case greater_or_equal: return left >= m_rhs; default: Q_ASSERT(!"Sanity check"); break; } return false; } version required() const { return m_rhs; } QString operationToString() const { QString result; switch (m_op) { case less: result = " < "; break; - case greather: + case greater: result = " > "; break; case equal: result = " = "; break; case not_equal: result = " != "; break; case less_or_equal: result = " <= "; break; - case greather_or_equal: + case greater_or_equal: result = " >= "; break; default: Q_ASSERT(!"Sanity check"); break; } return result; } static version_checker fromString(const QString& version_info) { version_checker checker(invalid); if (version_info.isEmpty()) return checker; bool lookup_next_char = false; int strip_lead_pos = 0; switch (version_info.at(0).toLatin1()) { case '<': checker.m_op = less; lookup_next_char = true; break; case '>': - checker.m_op = greather; + checker.m_op = greater; lookup_next_char = true; break; case '=': strip_lead_pos = 1; checker.m_op = equal; break; default: strip_lead_pos = 0; checker.m_op = equal; break; } if (lookup_next_char) { if (version_info.at(1).toLatin1() == '=') { // NOTE Shift state checker.m_op = operation(int(checker.m_op) + 1); strip_lead_pos = 2; } else { strip_lead_pos = 1; } } // QString rhs_str = version_info.mid(strip_lead_pos).trimmed(); version rhs = version::fromString(rhs_str); if (rhs.isValid()) checker.bind_second(rhs); else checker.m_op = invalid; return checker; } private: operation m_op; version m_rhs; }; } // namespace PyKrita #endif // __VERSION_CHECKER_H__ diff --git a/plugins/extensions/qmic/QMic.cpp b/plugins/extensions/qmic/QMic.cpp index 5e292c7145..3454f30da2 100644 --- a/plugins/extensions/qmic/QMic.cpp +++ b/plugins/extensions/qmic/QMic.cpp @@ -1,464 +1,464 @@ /* * Copyright (c) 2017 Boudewijn Rempt * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "QMic.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_input_output_mapper.h" #include "kis_qmic_simple_convertor.h" #include "kis_import_qmic_processing_visitor.h" #include #include "kis_qmic_applicator.h" static const char ack[] = "ack"; K_PLUGIN_FACTORY_WITH_JSON(QMicFactory, "kritaqmic.json", registerPlugin();) QMic::QMic(QObject *parent, const QVariantList &) : KisActionPlugin(parent) , m_gmicApplicator(0) { #ifndef Q_OS_MAC KisPreferenceSetRegistry *preferenceSetRegistry = KisPreferenceSetRegistry::instance(); PluginSettingsFactory* settingsFactory = new PluginSettingsFactory(); preferenceSetRegistry->add("QMicPluginSettingsFactory", settingsFactory); m_qmicAction = createAction("QMic"); m_qmicAction->setActivationFlags(KisAction::ACTIVE_DEVICE); connect(m_qmicAction , SIGNAL(triggered()), this, SLOT(slotQMic())); m_againAction = createAction("QMicAgain"); m_againAction->setActivationFlags(KisAction::ACTIVE_DEVICE); m_againAction->setEnabled(false); connect(m_againAction, SIGNAL(triggered()), this, SLOT(slotQMicAgain())); m_gmicApplicator = new KisQmicApplicator(); connect(m_gmicApplicator, SIGNAL(gmicFinished(bool, int, QString)), this, SLOT(slotGmicFinished(bool, int, QString))); #endif } QMic::~QMic() { Q_FOREACH(QSharedMemory *memorySegment, m_sharedMemorySegments) { // dbgPlugins << "detaching" << memorySegment->key(); memorySegment->detach(); } qDeleteAll(m_sharedMemorySegments); m_sharedMemorySegments.clear(); if (m_pluginProcess) { m_pluginProcess->close(); } delete m_gmicApplicator; delete m_localServer; } void QMic::slotQMicAgain() { slotQMic(true); } void QMic::slotQMic(bool again) { m_qmicAction->setEnabled(false); m_againAction->setEnabled(false); // find the krita-gmic-qt plugin QString pluginPath = PluginSettings::gmicQtPath(); if (pluginPath.isEmpty() || !QFileInfo(pluginPath).exists() || !QFileInfo(pluginPath).isFile()) { QMessageBox::warning(0, i18nc("@title:window", "Krita"), i18n("Krita cannot find the gmic-qt plugin. You can set the location of the gmic-qt plugin in Settings/Configure Krita.")); return; } m_key = QUuid::createUuid().toString(); m_localServer = new QLocalServer(); m_localServer->listen(m_key); connect(m_localServer, SIGNAL(newConnection()), SLOT(connected())); m_pluginProcess = new QProcess(this); connect(viewManager(), SIGNAL(destroyed(QObject *o)), m_pluginProcess, SLOT(terminate())); m_pluginProcess->setProcessChannelMode(QProcess::ForwardedChannels); connect(m_pluginProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(pluginFinished(int,QProcess::ExitStatus))); connect(m_pluginProcess, SIGNAL(stateChanged(QProcess::ProcessState)), this, SLOT(pluginStateChanged(QProcess::ProcessState))); m_pluginProcess->start(pluginPath, QStringList() << m_key << (again ? QString(" reapply") : QString())); bool r = m_pluginProcess->waitForStarted(); while (m_pluginProcess->waitForFinished(10)) { qApp->processEvents(QEventLoop::ExcludeUserInputEvents); } dbgPlugins << "Plugin started" << r << m_pluginProcess->state(); } void QMic::connected() { dbgPlugins << "connected"; if (!viewManager()) return; QLocalSocket *socket = m_localServer->nextPendingConnection(); if (!socket) { return; } while (socket->bytesAvailable() < static_cast(sizeof(quint32))) { if (!socket->isValid()) { // stale request return; } socket->waitForReadyRead(1000); } QDataStream ds(socket); QByteArray msg; quint32 remaining; ds >> remaining; msg.resize(remaining); int got = 0; char* uMsgBuf = msg.data(); // FIXME: Should use read transaction for Qt >= 5.7: // https://doc.qt.io/qt-5/qdatastream.html#using-read-transactions do { got = ds.readRawData(uMsgBuf, remaining); remaining -= got; uMsgBuf += got; } while (remaining && got >= 0 && socket->waitForReadyRead(2000)); if (got < 0) { qWarning() << "Message reception failed" << socket->errorString(); delete socket; m_localServer->close(); delete m_localServer; m_localServer = 0; return; } QString message = QString::fromUtf8(msg); dbgPlugins << "Received" << message; // Check the message: we can get three different ones QMultiMap messageMap; Q_FOREACH(QString line, message.split('\n', QString::SkipEmptyParts)) { QList kv = line.split('=', QString::SkipEmptyParts); if (kv.size() == 2) { messageMap.insert(kv[0], kv[1]); } else { qWarning() << "line" << line << "is invalid."; } } if (!messageMap.contains("command")) { qWarning() << "Message did not contain a command"; return; } int mode = 0; if (messageMap.contains("mode")) { mode = messageMap.values("mode").first().toInt(); } QByteArray ba; QString messageBoxWarningText; if (messageMap.values("command").first() == "gmic_qt_get_image_size") { KisSelectionSP selection = viewManager()->image()->globalSelection(); if (selection) { QRect selectionRect = selection->selectedExactRect(); ba = QByteArray::number(selectionRect.width()) + "," + QByteArray::number(selectionRect.height()); } else { ba = QByteArray::number(viewManager()->image()->width()) + "," + QByteArray::number(viewManager()->image()->height()); } } else if (messageMap.values("command").first() == "gmic_qt_get_cropped_images") { // Parse the message, create the shared memory segments, and create a new message to send back and waid for ack QRectF cropRect(0.0, 0.0, 1.0, 1.0); if (!messageMap.contains("croprect") || messageMap.values("croprect").first().split(',', QString::SkipEmptyParts).size() != 4) { qWarning() << "gmic-qt didn't send a croprect or not a valid croprect"; } else { QStringList cr = messageMap.values("croprect").first().split(',', QString::SkipEmptyParts); cropRect.setX(cr[0].toFloat()); cropRect.setY(cr[1].toFloat()); cropRect.setWidth(cr[2].toFloat()); cropRect.setHeight(cr[3].toFloat()); } if (!prepareCroppedImages(&ba, cropRect, mode)) { qWarning() << "Failed to prepare images for gmic-qt"; } } else if (messageMap.values("command").first() == "gmic_qt_output_images") { // Parse the message. read the shared memory segments, fix up the current image and send an ack dbgPlugins << "gmic_qt_output_images"; QStringList layers = messageMap.values("layer"); m_outputMode = (OutputMode)mode; if (m_outputMode != IN_PLACE) { messageBoxWarningText = i18n("Sorry, this output mode is not implemented yet."); m_outputMode = IN_PLACE; } slotStartApplicator(layers); } else if (messageMap.values("command").first() == "gmic_qt_detach") { Q_FOREACH(QSharedMemory *memorySegment, m_sharedMemorySegments) { dbgPlugins << "detaching" << memorySegment->key() << memorySegment->isAttached(); if (memorySegment->isAttached()) { if (!memorySegment->detach()) { dbgPlugins << "\t" << memorySegment->error() << memorySegment->errorString(); } } } qDeleteAll(m_sharedMemorySegments); m_sharedMemorySegments.clear(); } else { qWarning() << "Received unknown command" << messageMap.values("command"); } dbgPlugins << "Sending" << QString::fromUtf8(ba); // HACK: Make sure QDataStream does not refuse to write! // Proper fix: Change the above read to use read transaction ds.resetStatus(); ds.writeBytes(ba.constData(), ba.length()); // Flush the socket because we might not return to the event loop! if (!socket->waitForBytesWritten(2000)) { qWarning() << "Failed to write response:" << socket->error(); } // Wait for the ack bool r = true; r &= socket->waitForReadyRead(2000); // wait for ack r &= (socket->read(qstrlen(ack)) == ack); if (!socket->waitForDisconnected(2000)) { qWarning() << "Remote not disconnected:" << socket->error(); // Wait again socket->disconnectFromServer(); if (socket->waitForDisconnected(2000)) { qWarning() << "Disconnect timed out:" << socket->error(); } } if (!messageBoxWarningText.isEmpty()) { // Defer the message box to the event loop QTimer::singleShot(0, [messageBoxWarningText]() { QMessageBox::warning(KisPart::instance()->currentMainwindow(), i18nc("@title:window", "Krita"), messageBoxWarningText); }); } } void QMic::pluginStateChanged(QProcess::ProcessState state) { dbgPlugins << "stateChanged" << state; } void QMic::pluginFinished(int exitCode, QProcess::ExitStatus exitStatus) { dbgPlugins << "pluginFinished" << exitCode << exitStatus; delete m_pluginProcess; m_pluginProcess = 0; delete m_localServer; m_localServer = 0; m_qmicAction->setEnabled(true); m_againAction->setEnabled(true); } void QMic::slotGmicFinished(bool successfully, int milliseconds, const QString &msg) { dbgPlugins << "slotGmicFinished();" << successfully << milliseconds << msg; if (successfully) { m_gmicApplicator->finish(); } else { m_gmicApplicator->cancel(); QMessageBox::warning(0, i18nc("@title:window", "Krita"), i18n("G'Mic failed, reason:") + msg); } } void QMic::slotStartApplicator(QStringList gmicImages) { dbgPlugins << "slotStartApplicator();" << gmicImages; if (!viewManager()) return; // Create a vector of gmic images QVector *> images; Q_FOREACH(const QString &image, gmicImages) { QStringList parts = image.split(',', QString::SkipEmptyParts); Q_ASSERT(parts.size() == 4); QString key = parts[0]; QString layerName = QByteArray::fromHex(parts[1].toLatin1()); int spectrum = parts[2].toInt(); int width = parts[3].toInt(); int height = parts[4].toInt(); dbgPlugins << key << layerName << width << height; QSharedMemory m(key); if (!m.attach(QSharedMemory::ReadOnly)) { qWarning() << "Could not attach to shared memory area." << m.error() << m.errorString(); } if (m.isAttached()) { if (!m.lock()) { - dbgPlugins << "Could not lock memeory segment" << m.error() << m.errorString(); + dbgPlugins << "Could not lock memory segment" << m.error() << m.errorString(); } dbgPlugins << "Memory segment" << key << m.size() << m.constData() << m.data(); gmic_image *gimg = new gmic_image(); gimg->assign(width, height, 1, spectrum); gimg->name = layerName; gimg->_data = new float[width * height * spectrum * sizeof(float)]; dbgPlugins << "width" << width << "height" << height << "size" << width * height * spectrum * sizeof(float) << "shared memory size" << m.size(); memcpy(gimg->_data, m.constData(), width * height * spectrum * sizeof(float)); dbgPlugins << "created gmic image" << gimg->name << gimg->_width << gimg->_height; if (!m.unlock()) { - dbgPlugins << "Could not unlock memeory segment" << m.error() << m.errorString(); + dbgPlugins << "Could not unlock memory segment" << m.error() << m.errorString(); } if (!m.detach()) { - dbgPlugins << "Could not detach from memeory segment" << m.error() << m.errorString(); + dbgPlugins << "Could not detach from memory segment" << m.error() << m.errorString(); } images.append(gimg); } } dbgPlugins << "Got" << images.size() << "gmic images"; // Start the applicator KUndo2MagicString actionName = kundo2_i18n("Gmic filter"); KisNodeSP rootNode = viewManager()->image()->root(); KisInputOutputMapper mapper(viewManager()->image(), viewManager()->activeNode()); KisNodeListSP layers = mapper.inputNodes(m_inputMode); m_gmicApplicator->setProperties(viewManager()->image(), rootNode, images, actionName, layers); m_gmicApplicator->apply(); m_gmicApplicator->finish(); } bool QMic::prepareCroppedImages(QByteArray *message, QRectF &rc, int inputMode) { if (!viewManager()) return false; viewManager()->image()->lock(); m_inputMode = (InputLayerMode)inputMode; dbgPlugins << "prepareCroppedImages()" << QString::fromUtf8(*message) << rc << inputMode; KisInputOutputMapper mapper(viewManager()->image(), viewManager()->activeNode()); KisNodeListSP nodes = mapper.inputNodes(m_inputMode); if (nodes->isEmpty()) { viewManager()->image()->unlock(); return false; } for (int i = 0; i < nodes->size(); ++i) { KisNodeSP node = nodes->at(i); if (node && node->paintDevice()) { QRect cropRect; KisSelectionSP selection = viewManager()->image()->globalSelection(); if (selection) { cropRect = selection->selectedExactRect(); } else { cropRect = viewManager()->image()->bounds(); } dbgPlugins << "Converting node" << node->name() << cropRect; const QRectF mappedRect = KisAlgebra2D::mapToRect(cropRect).mapRect(rc); const QRect resultRect = mappedRect.toAlignedRect(); QSharedMemory *m = new QSharedMemory(QString("key_%1").arg(QUuid::createUuid().toString())); m_sharedMemorySegments.append(m); if (!m->create(resultRect.width() * resultRect.height() * 4 * sizeof(float))) { //buf.size())) { qWarning() << "Could not create shared memory segment" << m->error() << m->errorString(); return false; } m->lock(); gmic_image img; img.assign(resultRect.width(), resultRect.height(), 1, 4); img._data = reinterpret_cast(m->data()); KisQmicSimpleConvertor::convertToGmicImageFast(node->paintDevice(), &img, resultRect); message->append(m->key().toUtf8()); m->unlock(); message->append(","); message->append(node->name().toUtf8().toHex()); message->append(","); message->append(QByteArray::number(resultRect.width())); message->append(","); message->append(QByteArray::number(resultRect.height())); message->append("\n"); } } dbgPlugins << QString::fromUtf8(*message); viewManager()->image()->unlock(); return true; } #include "QMic.moc" diff --git a/plugins/filters/blur/kis_gaussian_blur_filter.cpp b/plugins/filters/blur/kis_gaussian_blur_filter.cpp index 40450579d5..220bfc501f 100644 --- a/plugins/filters/blur/kis_gaussian_blur_filter.cpp +++ b/plugins/filters/blur/kis_gaussian_blur_filter.cpp @@ -1,122 +1,122 @@ /* * This file is part of Krita * * Copyright (c) 2009 Edward Apap * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_gaussian_blur_filter.h" #include "kis_wdg_gaussian_blur.h" #include #include #include #include #include "ui_wdg_gaussian_blur.h" #include #include #include #include #include "kis_lod_transform.h" #include KisGaussianBlurFilter::KisGaussianBlurFilter() : KisFilter(id(), categoryBlur(), i18n("&Gaussian Blur...")) { setSupportsPainting(true); setSupportsAdjustmentLayers(true); setSupportsLevelOfDetail(true); setColorSpaceIndependence(FULLY_INDEPENDENT); } KisConfigWidget * KisGaussianBlurFilter::createConfigurationWidget(QWidget* parent, const KisPaintDeviceSP) const { return new KisWdgGaussianBlur(parent); } KisFilterConfigurationSP KisGaussianBlurFilter::factoryConfiguration() const { KisFilterConfigurationSP config = new KisFilterConfiguration(id().id(), 1); config->setProperty("horizRadius", 5); config->setProperty("vertRadius", 5); config->setProperty("lockAspect", true); return config; } void KisGaussianBlurFilter::processImpl(KisPaintDeviceSP device, const QRect& rect, const KisFilterConfigurationSP _config, KoUpdater* progressUpdater ) const { Q_ASSERT(device != 0); KisFilterConfigurationSP config = _config ? _config : new KisFilterConfiguration(id().id(), 1); KisLodTransformScalar t(device); QVariant value; config->getProperty("horizRadius", value); float horizontalRadius = t.scale(value.toFloat()); config->getProperty("vertRadius", value); float verticalRadius = t.scale(value.toFloat()); QBitArray channelFlags; if (config) { channelFlags = config->channelFlags(); } if (channelFlags.isEmpty() || !config) { channelFlags = QBitArray(device->colorSpace()->channelCount(), true); } KisGaussianKernel::applyGaussian(device, rect, horizontalRadius, verticalRadius, channelFlags, progressUpdater); } QRect KisGaussianBlurFilter::neededRect(const QRect & rect, const KisFilterConfigurationSP _config, int lod) const { KisLodTransformScalar t(lod); QVariant value; /** - * NOTE: integer devision by two is done on purpose, + * NOTE: integer division by two is done on purpose, * because the kernel size is always odd */ const int halfWidth = _config->getProperty("horizRadius", value) ? KisGaussianKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; const int halfHeight = _config->getProperty("vertRadius", value) ? KisGaussianKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; return rect.adjusted(-halfWidth * 2, -halfHeight * 2, halfWidth * 2, halfHeight * 2); } QRect KisGaussianBlurFilter::changedRect(const QRect & rect, const KisFilterConfigurationSP _config, int lod) const { KisLodTransformScalar t(lod); QVariant value; const int halfWidth = _config->getProperty("horizRadius", value) ? KisGaussianKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; const int halfHeight = _config->getProperty("vertRadius", value) ? KisGaussianKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; return rect.adjusted( -halfWidth, -halfHeight, halfWidth, halfHeight); } diff --git a/plugins/filters/convertheightnormalmap/kis_convert_height_to_normal_map_filter.cpp b/plugins/filters/convertheightnormalmap/kis_convert_height_to_normal_map_filter.cpp index 8ee397a572..dc8eef1304 100644 --- a/plugins/filters/convertheightnormalmap/kis_convert_height_to_normal_map_filter.cpp +++ b/plugins/filters/convertheightnormalmap/kis_convert_height_to_normal_map_filter.cpp @@ -1,169 +1,169 @@ /* * Copyright (c) 2017 Wolthera van Hövell tot Westerflier * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_convert_height_to_normal_map_filter.h" #include "kis_wdg_convert_height_to_normal_map.h" #include #include #include #include #include "kis_lod_transform.h" #include K_PLUGIN_FACTORY_WITH_JSON(KritaConvertHeightToNormalMapFilterFactory, "kritaconvertheighttonormalmap.json", registerPlugin();) KritaConvertHeightToNormalMapFilter::KritaConvertHeightToNormalMapFilter(QObject *parent, const QVariantList &) : QObject(parent) { KisFilterRegistry::instance()->add(KisFilterSP(new KisConvertHeightToNormalMapFilter())); } KritaConvertHeightToNormalMapFilter::~KritaConvertHeightToNormalMapFilter() { } KisConvertHeightToNormalMapFilter::KisConvertHeightToNormalMapFilter(): KisFilter(id(), categoryEdgeDetection(), i18n("&Height to Normal Map...")) { setSupportsPainting(true); setSupportsAdjustmentLayers(true); setSupportsLevelOfDetail(true); setColorSpaceIndependence(FULLY_INDEPENDENT); setShowConfigurationWidget(true); } void KisConvertHeightToNormalMapFilter::processImpl(KisPaintDeviceSP device, const QRect &rect, const KisFilterConfigurationSP config, KoUpdater *progressUpdater) const { Q_ASSERT(device != 0); KisFilterConfigurationSP configuration = config ? config : new KisFilterConfiguration(id().id(), 1); KisLodTransformScalar t(device); QVariant value; configuration->getProperty("horizRadius", value); float horizontalRadius = t.scale(value.toFloat()); configuration->getProperty("vertRadius", value); float verticalRadius = t.scale(value.toFloat()); QBitArray channelFlags; if (configuration) { channelFlags = configuration->channelFlags(); } if (channelFlags.isEmpty() || !configuration) { channelFlags = device->colorSpace()->channelFlags(); } KisEdgeDetectionKernel::FilterType type = KisEdgeDetectionKernel::SobelVector; if (configuration->getString("type") == "prewitt") { type = KisEdgeDetectionKernel::Prewit; } else if (configuration->getString("type") == "simple") { type = KisEdgeDetectionKernel::Simple; } int channelToConvert = configuration->getInt("channelToConvert", 0); QVector channelOrder(3); QVector channelFlip(3); channelFlip.fill(false); int i = config->getInt("redSwizzle", 0); if (i%2==1 || i==2) { channelFlip[0] = true; } if (i==3) { channelFlip[0] = false; } channelOrder[device->colorSpace()->channels().at(0)->displayPosition()] = qMax(i/2,0); i = config->getInt("greenSwizzle", 2); if (i%2==1 || i==2) { channelFlip[1] = true; } if (i==3) { channelFlip[1] = false; } channelOrder[device->colorSpace()->channels().at(1)->displayPosition()] = qMax(i/2,0); i = config->getInt("blueSwizzle", 4); if (i%2==1 || i==2) { channelFlip[2] = true; } if (i==3) { channelFlip[2] = false; } channelOrder[device->colorSpace()->channels().at(2)->displayPosition()] = qMax(i/2,0); KisEdgeDetectionKernel::convertToNormalMap(device, rect, horizontalRadius, verticalRadius, type, channelToConvert, channelOrder, channelFlip, channelFlags, progressUpdater); } KisFilterConfigurationSP KisConvertHeightToNormalMapFilter::factoryConfiguration() const { KisFilterConfigurationSP config = new KisFilterConfiguration(id().id(), 1); config->setProperty("horizRadius", 1); config->setProperty("vertRadius", 1); config->setProperty("type", "sobol"); config->setProperty("channelToConvert", 0); config->setProperty("lockAspect", true); config->setProperty("redSwizzle", KisWdgConvertHeightToNormalMap::xPlus); config->setProperty("greenSwizzle", KisWdgConvertHeightToNormalMap::yPlus); config->setProperty("blueSwizzle", KisWdgConvertHeightToNormalMap::zPlus); return config; } KisConfigWidget *KisConvertHeightToNormalMapFilter::createConfigurationWidget(QWidget *parent, const KisPaintDeviceSP dev) const { return new KisWdgConvertHeightToNormalMap(parent, dev->colorSpace()); } QRect KisConvertHeightToNormalMapFilter::neededRect(const QRect &rect, const KisFilterConfigurationSP _config, int lod) const { KisLodTransformScalar t(lod); QVariant value; /** - * NOTE: integer devision by two is done on purpose, + * NOTE: integer division by two is done on purpose, * because the kernel size is always odd */ const int halfWidth = _config->getProperty("horizRadius", value) ? KisEdgeDetectionKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; const int halfHeight = _config->getProperty("vertRadius", value) ? KisEdgeDetectionKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; return rect.adjusted(-halfWidth * 2, -halfHeight * 2, halfWidth * 2, halfHeight * 2); } QRect KisConvertHeightToNormalMapFilter::changedRect(const QRect &rect, const KisFilterConfigurationSP _config, int lod) const { KisLodTransformScalar t(lod); QVariant value; const int halfWidth = _config->getProperty("horizRadius", value) ? KisEdgeDetectionKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; const int halfHeight = _config->getProperty("vertRadius", value) ? KisEdgeDetectionKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; return rect.adjusted( -halfWidth, -halfHeight, halfWidth, halfHeight); } #include "kis_convert_height_to_normal_map_filter.moc" diff --git a/plugins/filters/edgedetection/kis_edge_detection_filter.cpp b/plugins/filters/edgedetection/kis_edge_detection_filter.cpp index 3bd48dbb19..1ecf44a562 100644 --- a/plugins/filters/edgedetection/kis_edge_detection_filter.cpp +++ b/plugins/filters/edgedetection/kis_edge_detection_filter.cpp @@ -1,158 +1,158 @@ /* * Copyright (c) 2017 Wolthera van Hövell tot Westerflier * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_edge_detection_filter.h" #include "kis_wdg_edge_detection.h" #include #include #include #include #include #include #include #include #include #include "kis_lod_transform.h" #include #include #include K_PLUGIN_FACTORY_WITH_JSON(KritaEdgeDetectionFilterFactory, "kritaedgedetection.json", registerPlugin();) KritaEdgeDetectionFilter::KritaEdgeDetectionFilter(QObject *parent, const QVariantList &) : QObject(parent) { KisFilterRegistry::instance()->add(KisFilterSP(new KisEdgeDetectionFilter())); } KritaEdgeDetectionFilter::~KritaEdgeDetectionFilter() { } KisEdgeDetectionFilter::KisEdgeDetectionFilter(): KisFilter(id(), categoryEdgeDetection(), i18n("&Edge Detection...")) { setSupportsPainting(true); setSupportsAdjustmentLayers(true); setSupportsLevelOfDetail(true); setColorSpaceIndependence(FULLY_INDEPENDENT); setShowConfigurationWidget(true); } void KisEdgeDetectionFilter::processImpl(KisPaintDeviceSP device, const QRect &rect, const KisFilterConfigurationSP config, KoUpdater *progressUpdater) const { Q_ASSERT(device != 0); KisFilterConfigurationSP configuration = config ? config : new KisFilterConfiguration(id().id(), 1); KisLodTransformScalar t(device); QVariant value; configuration->getProperty("horizRadius", value); float horizontalRadius = t.scale(value.toFloat()); configuration->getProperty("vertRadius", value); float verticalRadius = t.scale(value.toFloat()); QBitArray channelFlags; if (configuration) { channelFlags = configuration->channelFlags(); } if (channelFlags.isEmpty() || !configuration) { channelFlags = device->colorSpace()->channelFlags(); } KisEdgeDetectionKernel::FilterType type = KisEdgeDetectionKernel::SobelVector; if (config->getString("type") == "prewitt") { type = KisEdgeDetectionKernel::Prewit; } else if (config->getString("type") == "simple") { type = KisEdgeDetectionKernel::Simple; } KisEdgeDetectionKernel::FilterOutput output = KisEdgeDetectionKernel::pythagorean; if (config->getString("output") == "xGrowth") { output = KisEdgeDetectionKernel::xGrowth; } else if (config->getString("output") == "xFall") { output = KisEdgeDetectionKernel::xFall; } else if (config->getString("output") == "yGrowth") { output = KisEdgeDetectionKernel::yGrowth; } else if (config->getString("output") == "yFall") { output = KisEdgeDetectionKernel::yFall; } else if (config->getString("output") == "radian") { output = KisEdgeDetectionKernel::radian; } KisEdgeDetectionKernel::applyEdgeDetection(device, rect, horizontalRadius, verticalRadius, type, channelFlags, progressUpdater, output, config->getBool("transparency", false)); } KisFilterConfigurationSP KisEdgeDetectionFilter::factoryConfiguration() const { KisFilterConfigurationSP config = new KisFilterConfiguration(id().id(), 1); config->setProperty("horizRadius", 1); config->setProperty("vertRadius", 1); config->setProperty("type", "prewitt"); config->setProperty("output", "pythagorean"); config->setProperty("lockAspect", true); config->setProperty("transparency", false); return config; } KisConfigWidget *KisEdgeDetectionFilter::createConfigurationWidget(QWidget *parent, const KisPaintDeviceSP dev) const { Q_UNUSED(dev); return new KisWdgEdgeDetection(parent); } QRect KisEdgeDetectionFilter::neededRect(const QRect &rect, const KisFilterConfigurationSP _config, int lod) const { KisLodTransformScalar t(lod); QVariant value; /** - * NOTE: integer devision by two is done on purpose, + * NOTE: integer division by two is done on purpose, * because the kernel size is always odd */ const int halfWidth = _config->getProperty("horizRadius", value) ? KisEdgeDetectionKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; const int halfHeight = _config->getProperty("vertRadius", value) ? KisEdgeDetectionKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; return rect.adjusted(-halfWidth * 2, -halfHeight * 2, halfWidth * 2, halfHeight * 2); } QRect KisEdgeDetectionFilter::changedRect(const QRect &rect, const KisFilterConfigurationSP _config, int lod) const { KisLodTransformScalar t(lod); QVariant value; const int halfWidth = _config->getProperty("horizRadius", value) ? KisEdgeDetectionKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; const int halfHeight = _config->getProperty("vertRadius", value) ? KisEdgeDetectionKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; return rect.adjusted( -halfWidth, -halfHeight, halfWidth, halfHeight); } #include "kis_edge_detection_filter.moc" diff --git a/plugins/flake/textshape/kotext/KoInlineObject.h b/plugins/flake/textshape/kotext/KoInlineObject.h index 41d3449593..02e635302a 100644 --- a/plugins/flake/textshape/kotext/KoInlineObject.h +++ b/plugins/flake/textshape/kotext/KoInlineObject.h @@ -1,239 +1,239 @@ /* This file is part of the KDE project * Copyright (C) 2006-2009 Thomas Zander * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOINLINEOBJECTBASE_H #define KOINLINEOBJECTBASE_H #include "kritatext_export.h" #include #include #include class QVariant; class QTextDocument; class QTextCharFormat; class QPaintDevice; class QPainter; class QRectF; class KoInlineTextObjectManager; class KoInlineObjectPrivate; class KoShapeSavingContext; class KoTextInlineRdf; class KoShapeLoadingContext; /** * Base class for all inline-text-objects. * * In a TextShape you can insert objects that move with the text. * They are essentially anchored to a specific position in the text, as * one character. * * @see KoInlineTextObjectManager */ class KRITATEXT_EXPORT KoInlineObject : public QObject { Q_OBJECT public: enum Property { DocumentURL, PageCount, AuthorName, SenderEmail, SenderCompany, SenderPhoneWork, SenderPhonePrivate, SenderFax, SenderCountry, Title, Keywords, Subject, Description, Comments, SenderPostalCode, SenderCity, SenderStreet, SenderTitle, SenderFirstname, SenderLastname, SenderPosition, AuthorInitials, Chapter, ///< Chapter (number, name, number and name, plain number, plain number and name) variables. KarbonStart = 1000, ///< Base number for Karbon specific values. KexiStart = 2000, ///< Base number for Kexi specific values. FlowStart = 3000, ///< Base number for Flow specific values. PlanStart = 4000, ///< Base number for Plan specific values. StageStart = 5000, ///< Base number for Stage specific values. KritaStart = 6000, ///< Base number for Krita specific values. WordsStart = 7000, ///< Base number for Words specific values. VariableManagerStart = 8000, ///< Start of numbers reserved for the KoVariableManager UserGet = 12000, ///< User defined variable user-field-get UserInput = 12001 ///< User defined variable user-field-input }; /** * constructor * @param propertyChangeListener if set to true this instance will be notified of changes of properties. * @see KoInlineTextObjectManager::setProperty() * @see propertyChangeListener() */ explicit KoInlineObject(bool propertyChangeListener = false); ~KoInlineObject() override; /** * Will be called by the manager when this variable is added. * Remember that inheriting classes should not use the manager() in the constructor, since it will be 0 * @param manager the object manager for this object. */ void setManager(KoInlineTextObjectManager *manager); /** * Return the object manager set on this inline object. */ KoInlineTextObjectManager *manager() const; /** * Just prior to the first time this object will be shown this method will be called. * The object plugin should reimplement this to initialize the object after the manager * has been set, but before the text has been layouted. * The default implementation does nothing. */ virtual void setup() {} /** * Save this inlineObject as ODF * @param context the context for saving. */ virtual void saveOdf(KoShapeSavingContext &context) = 0; /** * Update position of the inline object. * This is called each time the paragraph this inline object is in is re-layouted giving you the opportunity * to reposition your object based on the new information. * @param document the text document this inline object is operating on. * @param posInDocument the character position in the document (param document) this inline object is at. * @param format the character format for the inline object. */ virtual void updatePosition(const QTextDocument *document, int posInDocument, const QTextCharFormat &format) = 0; /** * Update the size of the inline object. * Each time the text is painted, as well as when the paragraph this variable is in, this method * is called. You should alter the size of the object if the content has changed. * Altering the size is done by altering the 'object' parameter using QTextInlineObject::setWidth(), * QTextInlineObject::setAscent() and QTextInlineObject::setDescent() methods. * Note that this method is called while painting; and thus is time sensitive; avoid doing anything time * consuming. * Note make sure that the width is 0 when there is nothing to be shown for the object. * @param document the text document this inline object is operating on. * @param object the inline object properties * @param posInDocument the character position in the document (param document) this inline object is at. * @param format the character format for the inline object. * @param pd the postscript-paintdevice that all text is rendered on. Use this for QFont and related * classes so the inline object can be reused on any paintdevice. */ virtual void resize(const QTextDocument *document, QTextInlineObject &object, int posInDocument, const QTextCharFormat &format, QPaintDevice *pd) = 0; /** * Paint the inline-object-base using the provided painter within the rectangle specified by rect. * @param document the text document this inline object is operating on. * @param object the inline object properties * @param posInDocument the character position in the document (param document) this inline object is at. * @param format the character format for the inline object. * @param pd the postscript-paintdevice that all text is rendered on. Use this for QFont and related * classes so the inline object can be reused on any paintdevice. * @param painter the painting object to paint on. Note that unline many places in calligra painting * should happen at the position indicated by the rect, not at top-left. * @param rect the rectangle inside which the variable can paint itself. Painting outside the rect * will give varous problems with regards to repainting issues. */ virtual void paint(QPainter &painter, QPaintDevice *pd, const QTextDocument *document, const QRectF &rect, const QTextInlineObject &object, int posInDocument, const QTextCharFormat &format) = 0; /** - * Overwrite this if you are interrested in propertychanges. + * Overwrite this if you are interested in propertychanges. * @param property the property id that has been changed, one from the Property enum. * You should ignore all properties you don't use as new properties can be added at any time. * @param value the new value of the property wrapped in a QVariant. Properties can be a lot of * different class types. Ints, bools, QStrings etc. * example: * @code * void KoDateVariable::propertyChanged(Property key, const QVariant &value) { * if(key == KoInlineObject::PageCount) * setValue(QString::number(value.toInt())); * } * @endcode * @see propertyChangeListener() */ virtual void propertyChanged(Property property, const QVariant &value); /// return the inline-object Id that is assigned for this object. int id() const; /// Set the inline-object Id that is assigned for this object by the KoInlineTextObjectManager. void setId(int id); /** * When true, notify this object of property changes. * Each inlineObject can use properties like the PageCount or the document name. * Only objects that actually have a need for such information be a listener to avoid unneeded * overhead. * When this returns true, the propertyChanged() method will be called. * @see KoInlineTextObjectManager::setProperty() */ bool propertyChangeListener() const; /** * An inline object might have some Rdf metadata associated with it * in content.xml * Ownership of the rdf object is taken by this object, you should not * delete it. */ void setInlineRdf(KoTextInlineRdf *rdf); /** * Get any Rdf which was stored in content.xml for this inline object * This object continues to own the object, do not delete it. */ KoTextInlineRdf *inlineRdf() const; /** * Load a variable from odf. * * @param element element which represents the shape in odf * @param context the KoShapeLoadingContext used for loading * * @return false if loading failed */ virtual bool loadOdf(const KoXmlElement &element, KoShapeLoadingContext &context) = 0; protected: explicit KoInlineObject(KoInlineObjectPrivate &, bool propertyChangeListener = false); KoInlineObjectPrivate *d_ptr; private: Q_DECLARE_PRIVATE(KoInlineObject) friend KRITATEXT_EXPORT QDebug operator<<(QDebug, const KoInlineObject *); }; KRITATEXT_EXPORT QDebug operator<<(QDebug dbg, const KoInlineObject *o); #endif diff --git a/plugins/flake/textshape/kotext/KoSectionModel.h b/plugins/flake/textshape/kotext/KoSectionModel.h index 5e6f1f75cd..700c03ca46 100644 --- a/plugins/flake/textshape/kotext/KoSectionModel.h +++ b/plugins/flake/textshape/kotext/KoSectionModel.h @@ -1,142 +1,142 @@ #ifndef KOSECTIONMODEL_H #define KOSECTIONMODEL_H #include #include #include #include #include #include /** * Used to handle all the sections in the document * * Now there actually two levels of section handling: * 1) Formatting Level: on this level we should be sure, that * pointers to KoSection and KoSectionEnd in the KoParagraphStyles * properties SectionEndings and SectionStartings are consistent. * Handling on this level is provided on the level of text editing * commands: DeleteCommand, NewSectionCommand * We can't move it to another place, because we should know the * semantics of operation to handle it right way. * 2) Model(Tree) Level: on this level we should update KoSectionModel * right way, so it in any moment represents the actual tree - * of sections. Tree is builded easily: + * of sections. Tree is built easily: * One section is son of another, if it is directly nested in it. * As text editing commands have access to change Formatting Level, * they are declared as friend classes of KoSectionModel to be able * affect Model structure without changing something on Formatting * Level. Also affected by RenameSectionCommand. * * Also we need to look at the consistency of some section properties: * * 1) Bounds. Those now are handled with QTextCursors that are placed * on start and end of the section. In default state start cursor * isn't moving if text inserted in its position, and end cursor * moves. But in the case of initial document loading, it is necessary * to make some end cursors stop moving, so we have: * KoTextLoader -> calling -> KoSection::setKeepEndBound() * KoTextLoader -> calling -> KoSectionModel::allowMovingEndBound() * ^-- this needed to restore default behaviour after load * * 2) Level. Level means the depth of the section in tree. Root * sections has 0 (zero) level. Now if you look at the possible * text editing command affecting sections you may notice that * level of section doesn't change in any case. Initial level * is set in KoSection constructor as parent's level plus one. * TODO: write about drag-n-drop here, when its implemented * * 3) Name. Each KoSection has a name that must be unique. We have * two groups of KoSections in each moment of time: the first group * consists of the sections that are present in document now, * the second group consists of the sections that were deleted, but * we still need them as they may be restored with "undo". * This groups are stored in m_registeredSections and m_sectionNames. * * Sections are created through this newSection() and newSectionEnd() * functions. * * This object is created for QTextDocument on the first query of it. */ class KRITATEXT_EXPORT KoSectionModel : public QAbstractItemModel { Q_OBJECT public: static const int PointerRole = Qt::UserRole; explicit KoSectionModel(QTextDocument *doc); ~KoSectionModel() override; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; QModelIndex parent(const QModelIndex &child) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; /// Creates KoSection in position of @param cursor with some allowed name KoSection *createSection(const QTextCursor &cursor, KoSection *parent); /// Creates KoSection in position of @param cursor with specified @param name KoSection *createSection(const QTextCursor &cursor, KoSection *parent, const QString &name); /// Creates KoSectionEnd in pair for a @param section KoSectionEnd *createSectionEnd(KoSection *section); /** Tries to set @param section name to @param name * @return @c false if there is a section with such name * and new name isn't accepted and @c true otherwise. */ bool setName(KoSection *section, const QString &name); /** * Returns pointer to the deepest KoSection that covers @p pos * or 0 if there is no such section */ KoSection *sectionAtPosition(int pos); /// Returns name for the new section. QString possibleNewName(); /// Returns if this name is possible. bool isValidNewName(const QString &name) const; /// Setting all sections end bound cursor to move with text inserting. void allowMovingEndBound(); /// Finds index of @param child inside his parent. int findRowOfChild(KoSection *child) const; private: Q_DISABLE_COPY(KoSectionModel) friend class DeleteCommand; friend class NewSectionCommand; /** * Inserts @param section to it's parent (should be * stored in @param section already) in position childIdx. * Affects only Model Level(@see KoSectionModel). */ void insertToModel(KoSection* section, int childIdx); /** * Deletes @param section from it's parent (should be * stored in @param section already). * Affects only Model Level(@see KoSectionModel). */ void deleteFromModel(KoSection *section); QTextDocument *m_doc; QSet m_registeredSections; ///< stores pointer to sections that sometime was registered QHash m_sectionNames; ///< stores name -> pointer reference, for sections that are visible in document now QHash m_modelIndex; QVector m_rootSections; }; Q_DECLARE_METATYPE(KoSectionModel *) #endif //KOSECTIONMODEL_H diff --git a/plugins/flake/textshape/kotext/KoTextEditor_undo.cpp b/plugins/flake/textshape/kotext/KoTextEditor_undo.cpp index 47bac2a16b..1931f89195 100644 --- a/plugins/flake/textshape/kotext/KoTextEditor_undo.cpp +++ b/plugins/flake/textshape/kotext/KoTextEditor_undo.cpp @@ -1,329 +1,329 @@ /* This file is part of the KDE project * Copyright (C) 2009-2012 Pierre Stirnweiss * Copyright (C) 2006-2010 Thomas Zander * Copyright (c) 2011 Boudewijn Rempt * Copyright (C) 2011-2012 C. Boemann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoTextEditor.h" #include "KoTextEditor_p.h" #include "KoTextDocument.h" #include #include #include #include #include "TextDebug.h" /** Calligra's undo/redo framework. The @class KoTextEditor undo/redo framework sits between the @class QTextDocument and the apllication's undo/redo stack. When the @class QTextDocument is changed by an editing action, it internally creates an undo/redo command. When doing so a signal (undoCommandAdded()) is emitted by the @class QTextDocument in order for applications to update their undo/redo stack accordingly. Each @class QTextDocument used in Calligra is handled by a specific @class KoTextEditor. It is responsible for on the one hand edit the @class QTextDocument, and on the other hand to listen for the QTextDocument's signal. Calligra uses a @class KUndo2Stack as its application undo/redo stack. This stack is populated by @class KUndo2Command or sub-classes of it. In order to limit the number of command sub-classes, KoTextEditor provides a framework which uses a generic command. The framework is based on a sort of state machine. The KoTextEditor can be in several different states (see @enum KoTextEditor::Private::State). These are: NoOp: this states indicates that the KoTextEditor is not editing the QTextDocument. KeyPress: this state indicates that the user is typing text. All text typed in succession should correspond to one undo command. To be used when entering text outside of an insertTextCommand. Delete: this state indicates that the user is deleting characters. All deletions done in succession should correspond to one undo command. To be used for deleting outside a deleteCommand. Currently not in use, our deltion is done through a command because of inline objects. Format: this state indicates that we are formatting text. To be used when formatting outside of a command. Custom: this state indicates that the QTextDocument is changed through a KUndo2Command. KoTextEditor reacts differently when receiving the QTextDocument's signal, depending on its state. In addition the framework allows to encapsulate modifications in a on-the-fly created custom command (\sa beginEditBlock() endEditBlock()). Furthermore the framework allows to push complete KUndo2Commands. See the documentation file for how to use this framework. */ /* Important members: commandStack: This stack holds the headCommands. These parent the generated UndoTextCommands. When undo or redo is called, they will in turn call UndoTextCommand::undo/redo. editorState: Holds the state of the KoTextEditor. see above commandTitle: Holds the title which is to be used when creating a headCommand. addNewCommand: bool used to tell the framework to create a new headCommand and push it on the commandStack, when receiving an undoCommandAdded signal from QTextDocument. customCommandCount: counter used to keep track of nested KUndo2Commands that are pushed on the KoTextEditor. */ // This slot is called when the KoTextEditor receives the signal undoCommandAdded() from QTextDocument. A generic UndoTextCommand is used to match the QTextDocument's internal undo command. This command calls QTextDocument::undo() or QTextDocument::redo() respectively, which triggers the undo redo actions on the document. //In order to allow nesting commands, we maintain a stack of commands. The top command will be the parent of our auto generated UndoTextCommands. //Depending on the case, we might create a command which will serve as head command. This is pushed to our commandStack and eventually to the application's stack. void KoTextEditor::Private::documentCommandAdded() { class UndoTextCommand : public KUndo2Command { public: UndoTextCommand(QTextDocument *document, KoTextEditor::Private *p, KUndo2Command *parent = 0) : KUndo2Command(kundo2_i18n("Text"), parent), m_document(document) , m_p(p) {} void undo() override { QTextDocument *doc = m_document.data(); if (doc == 0) return; doc->undo(KoTextDocument(doc).textEditor()->cursor()); m_p->emitTextFormatChanged(); } void redo() override { QTextDocument *doc = m_document.data(); if (doc == 0) return; doc->redo(KoTextDocument(doc).textEditor()->cursor()); m_p->emitTextFormatChanged(); } QWeakPointer m_document; KoTextEditor::Private *m_p; }; debugText << "received a QTextDocument undoCommand signal"; debugText << "commandStack count: " << commandStack.count(); debugText << "addCommand: " << addNewCommand; debugText << "editorState: " << editorState; if (commandStack.isEmpty()) { //We have an empty stack. We need a head command which is to be pushed onto our commandStack and on the application stack if there is one. //This command will serve as a parent for the auto-generated UndoTextCommands. debugText << "empty stack, will push a new headCommand on both commandStack and application's stack. title: " << commandTitle; commandStack.push(new KUndo2Command(commandTitle)); if (KoTextDocument(document).undoStack()) { KoTextDocument(document).undoStack()->push(commandStack.top()); } addNewCommand = false; debugText << "commandStack is now: " << commandStack.count(); } else if (addNewCommand) { //We have already a headCommand on the commandStack. However we want a new child headCommand (nested commands) on the commandStack for parenting further UndoTextCommands. This shouldn't be pushed on the application's stack because it is a child of the current commandStack's top. debugText << "we have a headCommand on the commandStack but need a new child command. we will push it only on the commandStack: " << commandTitle; commandStack.push(new KUndo2Command(commandTitle, commandStack.top())); addNewCommand = false; debugText << "commandStack count is now: " << commandStack.count(); } else if ((editorState == KeyPress || editorState == Delete) && !commandStack.isEmpty() && commandStack.top()->childCount()) { //QTextDocument emits a signal on the first key press (or delete) and "merges" the subsequent ones, until an incompatible one is done. In which case it re-emit a signal. //Here we are in KeyPress (or Delete) state. The fact that the commandStack isn't empty and its top command has children means that we just received such a signal. We therefore need to pop the previous headCommand (which were either key press or delete) and create a new one to parent the UndoTextCommands. This command also need to be pushed on the application's stack. debugText << "we are in subsequent keyPress/delete state and still received a signal. we need to create a new headCommand: " << commandTitle; debugText << "so we pop the current one and push the new one on both the commandStack and the application's stack"; commandStack.pop(); commandStack.push(new KUndo2Command(commandTitle, !commandStack.isEmpty()?commandStack.top():0)); if (KoTextDocument(document).undoStack()) { KoTextDocument(document).undoStack()->push(commandStack.top()); } debugText << "commandStack count: " << commandStack.count(); } //Now we can create our UndoTextCommand which is parented to the commandStack't top headCommand. new UndoTextCommand(document, this, commandStack.top()); debugText << "done creating the dummy UndoTextCommand"; } //This method is used to update the KoTextEditor state, which will condition how the QTextDocument::undoCommandAdded signal will get handled. void KoTextEditor::Private::updateState(KoTextEditor::Private::State newState, const KUndo2MagicString &title) { debugText << "updateState from: " << editorState << " to: " << newState << " with: " << title; debugText << "commandStack count: " << commandStack.count(); if (editorState == Custom && newState != NoOp) { //We already are in a custom state (meaning that either a KUndo2Command was pushed on us, an on-the-fly macro command was started or we are executing a complex editing from within the KoTextEditor. //In that state any update of the state different from NoOp is part of that "macro". However, updating the state means that we are now wanting to have a new command for parenting the UndoTextCommand generated after the signal //from QTextDocument. This is to ensure that undo/redo actions are done in the proper order. Setting addNewCommand will ensure that we create such a child headCommand on the commandStack. This command will not be pushed on the application's stack. debugText << "we are already in a custom state. a new state, which is not NoOp is part of the macro we are doing. we need however to create a new command on the commandStack to parent a signal induced UndoTextCommand"; addNewCommand = true; if (!title.isEmpty()) commandTitle = title; else commandTitle = kundo2_i18n("Text"); debugText << "returning now. commandStack is not modified at this stage"; return; } if (newState == NoOp && !commandStack.isEmpty()) { //Calling updateState to NoOp when the commandStack isn't empty means that the current headCommand on the commandStack is finished. Further UndoTextCommands do not belong to it. So we pop it. - //If after poping the headCommand we still have some commands on the commandStack means we have not finished with the highest "macro". In that case we need to stay in the "Custom" state. + //If after popping the headCommand we still have some commands on the commandStack means we have not finished with the highest "macro". In that case we need to stay in the "Custom" state. //On the contrary, an empty commandStack means we have finished with the "macro". In that case, we set the editor to NoOp state. A signal from the QTextDocument should also generate a new headCommand. debugText << "we are in a macro and update the state to NoOp. this means that the command on top of the commandStack is finished. we should pop it"; debugText << "commandStack count before: " << commandStack.count(); commandStack.pop(); debugText << "commandStack count after: " << commandStack.count(); if (commandStack.isEmpty()) { debugText << "we have no more commands on the commandStack. the macro is complete. next signal induced command will need to be parented to a new headCommand. Also the editor should go to NoOp"; addNewCommand = true; editorState = NoOp; } debugText << "returning now. commandStack count: " << commandStack.count(); return; } if (editorState != newState || commandTitle != title) { //We are not in "Custom" state but either are moving to a new state (from editing to format,...) or the command type is the same, but not the command itself (like format:bold, format:italic). The later case is caracterised by a different command title. //When we change command, we need to pop the current commandStack's top and ask for a new headCommand to be created. debugText << "we are not in a custom state but change the command"; debugText << "commandStack count: " << commandStack.count(); if (!commandStack.isEmpty()) { debugText << "the commandStack is not empty. however the command on it is not a macro. so we pop it and ask to recreate a new one: " << title; commandStack.pop(); addNewCommand = true; } } editorState = newState; if (!title.isEmpty()) commandTitle = title; else commandTitle = kundo2_i18n("Text"); debugText << "returning now. commandStack count: " << commandStack.count(); } /// This method is used to push a complete KUndo2Command on the KoTextEditor. This command will be pushed on the application's stack if needed. The logic allows to push several commands which are then going to be nested, provided these children are pushed from within the redo method of their parent. void KoTextEditor::addCommand(KUndo2Command *command) { debugText << "we receive a command to add on the stack."; debugText << "commandStack count: " << d->commandStack.count(); debugText << "customCommandCount counter: " << d->customCommandCount << " will increase"; //We increase the customCommandCount counter to inform the framework that we are having a further KUndo2Command and update the KoTextEditor's state to Custom. //However, this update will request a new headCommand to be pushed on the commandStack. This is what we want for internal complex editions but not in this case. Indeed, it must be the KUndo2Command which will parent the UndoTextCommands. Therefore we set the addNewCommand back to false. //If the commandStack is empty, we are the highest "macro" command and we should therefore push the KUndo2Command on the application's stack. //On the contrary, if the commandStack is not empty, or the pushed command has a parent, it means that we are adding a nested KUndo2Command. In which case we just want to put it on the commandStack to parent UndoTextCommands. We need to call the redo method manually though. ++d->customCommandCount; debugText << "we will now go to custom state"; d->updateState(KoTextEditor::Private::Custom, (!command->text().isEmpty())?command->text():kundo2_i18n("Text")); debugText << "but will set the addCommand to false. we don't want a new headCommand"; d->addNewCommand = false; debugText << "commandStack count is: " << d->commandStack.count(); if (d->commandStack.isEmpty()) { debugText << "the commandStack is empty. this means we are the top most command"; d->commandStack.push(command); debugText << "command pushed on the commandStack. count: " << d->commandStack.count(); KUndo2QStack *stack = KoTextDocument(d->document).undoStack(); if (stack && !command->hasParent()) { debugText << "we have an application stack and the command is not a sub command of a non text command (which have been pushed outside kotext"; stack->push(command); debugText << "so we pushed it on the application's' stack"; } else { debugText << "we either have no application's stack, or our command is actually the child of a non kotext command"; command->redo(); debugText << "still called redo on it"; } } else { debugText << "the commandStack is not empty, our command is actually nested in another kotext command. we don't push on the application stack but only on the commandStack"; d->commandStack.push(command); debugText << "commandStack count after push: " << d->commandStack.count(); command->redo(); debugText << "called redo still"; } //When we reach that point, the command has been executed. We first need to clean up all the automatically generated headCommand on our commandStack, which could potentially have been created during the editing. When we reach our pushed command, the commandStack is clean. We can then call a state update to NoOp and decrease the customCommandCount counter. debugText << "the command has been executed. we need to clean up the commandStack of the auto generated headCommands"; debugText << "before cleaning. commandStack count: " << d->commandStack.count(); while (d->commandStack.top() != command) { d->commandStack.pop(); } debugText << "after cleaning. commandStack count: " << d->commandStack.count() << " will set NoOp"; d->updateState(KoTextEditor::Private::NoOp); debugText << "after NoOp set. inCustomCounter: " << d->customCommandCount << " will decrease and return"; --d->customCommandCount; } /// DO NOT USE THIS. It stays here for compiling reasons. But it will severely break everything. Again: DO NOT USE THIS. void KoTextEditor::instantlyExecuteCommand(KUndo2Command *command) { d->updateState(KoTextEditor::Private::Custom, (!command->text().isEmpty())?command->text():kundo2_i18n("Text")); command->redo(); // instant replay done let's not keep it dangling if (!command->hasParent()) { d->updateState(KoTextEditor::Private::NoOp); } } /// This method is used to start an on-the-fly macro command. Use KoTextEditor::endEditBlock to stop it. /// *** /// Important note: /// *** /// The framework does not allow to push a complete KUndo2Command (through KoTextEditor::addCommand) from within an EditBlock. Doing so will lead in the best case to several undo/redo commands on the application's stack instead of one, in the worst case to an out of sync application's stack. /// *** KUndo2Command *KoTextEditor::beginEditBlock(const KUndo2MagicString &title) { debugText << "beginEditBlock"; debugText << "commandStack count: " << d->commandStack.count(); debugText << "customCommandCount counter: " << d->customCommandCount; if (!d->customCommandCount) { // We are not in a custom macro command. So we first need to update the KoTextEditor's state to Custom. Additionally, if the commandStack is empty, we need to create a master headCommand for our macro and push it on the stack. debugText << "we are not in a custom command. will update state to custom"; d->updateState(KoTextEditor::Private::Custom, title); debugText << "commandStack count: " << d->commandStack.count(); if (d->commandStack.isEmpty()) { debugText << "the commandStack is empty. we need a dummy headCommand both on the commandStack and on the application's stack"; KUndo2Command *command = new KUndo2Command(title); d->commandStack.push(command); ++d->customCommandCount; d->dummyMacroAdded = true; //This bool is used to tell endEditBlock that we have created a master headCommand. KUndo2QStack *stack = KoTextDocument(d->document).undoStack(); if (stack) { stack->push(command); } else { command->redo(); } debugText << "done adding the headCommand. commandStack count: " << d->commandStack.count() << " inCommand counter: " << d->customCommandCount; } } //QTextDocument sends the undoCommandAdded signal at the end of the QTextCursor edit block. Since we want our master headCommand to parent the signal induced UndoTextCommands, we should not call QTextCursor::beginEditBlock for the headCommand. if (!(d->dummyMacroAdded && d->customCommandCount == 1)) { debugText << "we did not add a dummy command, or we are further down nesting. call beginEditBlock on the caret to nest the QTextDoc changes"; //we don't call beginEditBlock for the first headCommand because we want the signals to be sent before we finished our command. d->caret.beginEditBlock(); } debugText << "will return top od commandStack"; return (d->commandStack.isEmpty())?0:d->commandStack.top(); } void KoTextEditor::endEditBlock() { debugText << "endEditBlock"; //Only the self created master headCommand (see beginEditBlock) is left on the commandStack, we need to decrease the customCommandCount counter that we increased on creation. //If we are not yet at this master headCommand, we can call QTextCursor::endEditBlock if (d->dummyMacroAdded && d->customCommandCount == 1) { debugText << "only the created dummy headCommand from beginEditBlock is left. we need to decrease further the nesting counter"; //we don't call caret.endEditBlock because we did not begin a block for the first headCommand --d->customCommandCount; d->dummyMacroAdded = false; } else { debugText << "we are not at our top dummy headCommand. call caret.endEditBlock"; d->caret.endEditBlock(); } if (!d->customCommandCount) { //We have now finished completely the macro, set the editor state to NoOp then. debugText << "we have finished completely the macro, set the state to NoOp now. commandStack count: " << d->commandStack.count(); d->updateState(KoTextEditor::Private::NoOp); debugText << "done setting the state. editorState: " << d->editorState << " commandStack count: " << d->commandStack.count(); } } diff --git a/plugins/flake/textshape/kotext/KoTextLocator.h b/plugins/flake/textshape/kotext/KoTextLocator.h index 9eca5d4043..5013c60126 100644 --- a/plugins/flake/textshape/kotext/KoTextLocator.h +++ b/plugins/flake/textshape/kotext/KoTextLocator.h @@ -1,72 +1,72 @@ /* This file is part of the KDE project * Copyright (C) 2007 Thomas Zander * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOTEXTLOCATOR_H #define KOTEXTLOCATOR_H #include "KoInlineObject.h" #include "kritatext_export.h" class KoTextReference; /** * This inline object can be inserted in text to mark it and to later get location information from. * After inserting this locator you can request things like pageNumber() and chapter() for the * place where the locator has been positioned in the document. */ class KRITATEXT_EXPORT KoTextLocator : public KoInlineObject { Q_OBJECT public: /// constructor KoTextLocator(); ~KoTextLocator() override; /// reimplemented from super void updatePosition(const QTextDocument *document, int posInDocument, const QTextCharFormat &format) override; /// reimplemented from super void resize(const QTextDocument *document, QTextInlineObject &object, int posInDocument, const QTextCharFormat &format, QPaintDevice *pd) override; /// reimplemented from super void paint(QPainter &painter, QPaintDevice *pd, const QTextDocument *document, const QRectF &rect, const QTextInlineObject &object, int posInDocument, const QTextCharFormat &format) override; /// returns the text of the paragraph that is the first chapter before the index. QString chapter() const; /// return the page number on which the locator is placed. int pageNumber() const; /// return the position in the text document at which the locator is inserted. int indexPosition() const; /// return the word in which the locator is inserted. QString word() const; - /// Add a text reference that is interrested in knowing when this locator is laid-out in a differen position. + /// Add a text reference that is interested in knowing when this locator is laid-out in a different position. void addListener(KoTextReference *reference); /// Remove a reference from the listeners. void removeListener(KoTextReference *reference); bool loadOdf(const KoXmlElement &element, KoShapeLoadingContext &context) override; void saveOdf(KoShapeSavingContext &context) override; private: class Private; Private * const d; }; #endif diff --git a/plugins/flake/textshape/kotext/Mainpage.dox b/plugins/flake/textshape/kotext/Mainpage.dox index 6bda2cd2de..0d4bc2771e 100644 --- a/plugins/flake/textshape/kotext/Mainpage.dox +++ b/plugins/flake/textshape/kotext/Mainpage.dox @@ -1,65 +1,65 @@ /** * \mainpage * * KritaText is a library for general use that extends the QText framework * (codenamed scribe) with an enhanced text-layout which adds features * required by ODF and general word-processing applications. * * You can use KritaText at all places where you would normally use a * QTextDocument as the main text layout class in scribe can be replaced * on any QTextDocument instance using QTextDocument::setDocumentLayout(). * This means you can use the Qt API as normal, but you will be * able to use extra features like the plugins, the variables and the * ODF loading and saving for all the ODF text-layout features. * * TextShape Flake-Plugin * * Closely coupled with the kotext library is the text plugin that is * build on flake technology. All the user interaction dialogs and code * will reside in that plugin, and the actual heavy lifting of the * layout also is present only in that plugin. In other words; this * library will supply you with the APIs but without having the text * shape plugin loaded you can't show or layout the text. The goal is * to keep it cheap to link to this library and only provide the bare * minimum of functionality is the way to get there. The feature- * package will be completed by the optional text-plugin. * * QTextDocument compatibility * * The actual content is stored in the QTextDocument, as mentioned before. * In KoText we support a lot more features than Qt does in its layout * and this library will allow you to enrich your document with those * features. The core design goal is that you can use an externally * created QTextDocument with KoText. This has the implication that all * the extra content is stored inside the document. We add QTextFormat * based properties for that as can be seen in the styles (see - * KoParagraphStyle::Properities for instance), and we allow managers to + * KoParagraphStyle::Properties for instance), and we allow managers to * be stored on the document too. So for example a KoStyleManager will * be stored as a property on the QTextDocument and you can access that * using the KoTextDocument API. Note that you can use the * KoTextDocument class while using only a QTextDocument instance. * * Plugins * * There are various plugins for KoText that make it possible for 3rd * parties to extend the KoText functionality, see the techbase page; * http://techbase.kde.org/Development/Tutorials/Calligra_Overview#Text_Plugins * * ODF compatibility * * Loading and saving of documents can be done to and from ODF using the * open document classes. * * Important classes; * * KoTextDocumentLayout the main layout class to be set on the QTextDocument. * * KoInlineTextObject plugin base for inline objects (and variables) * * KoTextEditingPlugin plugin base for text editing plugins. * * KoText namespace. */ // DOXYGEN_SET_PROJECT_NAME = KritaText // DOXYGEN_SET_IGNORE_PREFIX = Ko K diff --git a/plugins/flake/textshape/kotext/opendocument/KoTextSharedLoadingData.cpp b/plugins/flake/textshape/kotext/opendocument/KoTextSharedLoadingData.cpp index de44b8e404..3fedb59184 100644 --- a/plugins/flake/textshape/kotext/opendocument/KoTextSharedLoadingData.cpp +++ b/plugins/flake/textshape/kotext/opendocument/KoTextSharedLoadingData.cpp @@ -1,712 +1,712 @@ /* This file is part of the KDE project * Copyright (C) 2004-2006 David Faure * Copyright (C) 2007, 2010 Thomas Zander * Copyright (C) 2007 Sebastian Sauer * Copyright (C) 2007 Pierre Ducroquet * Copyright (C) 2007-2008 Thorsten Zachmann * Copyright (C) 2008 Girish Ramakrishnan * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoTextSharedLoadingData.h" #include #include #include #include #include #include #include #include #include "styles/KoStyleManager.h" #include "styles/KoParagraphStyle.h" #include "styles/KoCharacterStyle.h" #include "styles/KoListStyle.h" #include "styles/KoListLevelProperties.h" #include "styles/KoTableStyle.h" #include "styles/KoTableColumnStyle.h" #include "styles/KoTableRowStyle.h" #include "styles/KoTableCellStyle.h" #include "styles/KoSectionStyle.h" #include "KoOdfNotesConfiguration.h" #include "KoOdfBibliographyConfiguration.h" #include "KoTextTableTemplate.h" #include "TextDebug.h" class Q_DECL_HIDDEN KoTextSharedLoadingData::Private { public: Private() : defaultCharacterStyle(0) , defaultParagraphStyle(0) {} ~Private() { qDeleteAll(paragraphStylesToDelete); qDeleteAll(characterStylesToDelete); qDeleteAll(listStylesToDelete); qDeleteAll(tableStylesToDelete); qDeleteAll(tableCellStylesToDelete); qDeleteAll(tableColumnStylesToDelete); qDeleteAll(tableRowStylesToDelete); qDeleteAll(sectionStylesToDelete); qDeleteAll(tableTemplatesToDelete); } // It is possible that automatic-styles in content.xml and styles.xml have the same name // within the same family. Therefore we have to keep them separate. The office:styles are // added to the autostyles so that only one lookup is needed to get the style. This is // about 30% faster than having a special data structure for office:styles. QHash paragraphContentDotXmlStyles; QHash characterContentDotXmlStyles; QHash listContentDotXmlStyles; QHash tableContentDotXmlStyles; QHash tableColumnContentDotXmlStyles; QHash tableRowContentDotXmlStyles; QHash tableCellContentDotXmlStyles; QHash sectionContentDotXmlStyles; QHash paragraphStylesDotXmlStyles; QHash characterStylesDotXmlStyles; QHash listStylesDotXmlStyles; QHash outlineStylesDotXmlStyles; QHash tableStylesDotXmlStyles; QHash tableColumnStylesDotXmlStyles; QHash tableRowStylesDotXmlStyles; QHash tableCellStylesDotXmlStyles; QHash sectionStylesDotXmlStyles; QHash tableTemplates; QList paragraphStylesToDelete; QList characterStylesToDelete; QList listStylesToDelete; QList tableStylesToDelete; QList tableCellStylesToDelete; QList tableColumnStylesToDelete; QList tableRowStylesToDelete; QList sectionStylesToDelete; QList tableTemplatesToDelete; QHash namedParagraphStyles; KoOdfBibliographyConfiguration bibliographyConfiguration; KoCharacterStyle *defaultCharacterStyle; KoParagraphStyle *defaultParagraphStyle; QList insertedShapes; }; KoTextSharedLoadingData::KoTextSharedLoadingData() : d(new Private()) { } KoTextSharedLoadingData::~KoTextSharedLoadingData() { delete d; } void KoTextSharedLoadingData::addDefaultCharacterStyle(KoShapeLoadingContext &context, const KoXmlElement *styleElem, const KoXmlElement *appDefault, KoStyleManager *styleManager) { if (styleManager) { if (styleElem) { styleManager->defaultCharacterStyle()->loadOdf(styleElem, context); } else if (appDefault) { styleManager->defaultCharacterStyle()->loadOdf(appDefault, context); } d->defaultCharacterStyle = styleManager->defaultCharacterStyle(); } } void KoTextSharedLoadingData::addDefaultParagraphStyle(KoShapeLoadingContext &context, const KoXmlElement *styleElem, const KoXmlElement *appDefault, KoStyleManager *styleManager) { if (styleManager) { if (styleElem) { styleManager->defaultParagraphStyle()->loadOdf(styleElem, context); } else if (appDefault) { styleManager->defaultParagraphStyle()->loadOdf(appDefault, context); } d->defaultParagraphStyle = styleManager->defaultParagraphStyle(); } } void KoTextSharedLoadingData::loadOdfStyles(KoShapeLoadingContext &shapeContext, KoStyleManager *styleManager) { KoOdfLoadingContext &context = shapeContext.odfLoadingContext(); // only add styles of office:styles to the style manager addDefaultCharacterStyle(shapeContext, context.stylesReader().defaultStyle("text"), context.defaultStylesReader().defaultStyle("text"), styleManager); addCharacterStyles(shapeContext, context.stylesReader().customStyles("text").values(), ContentDotXml | StylesDotXml, styleManager); addCharacterStyles(shapeContext, context.stylesReader().autoStyles("text", true).values(), StylesDotXml); addCharacterStyles(shapeContext, context.stylesReader().autoStyles("text").values(), ContentDotXml); addListStyles(shapeContext, context.stylesReader().autoStyles("list").values(), ContentDotXml); addListStyles(shapeContext, context.stylesReader().autoStyles("list", true).values(), StylesDotXml); addListStyles(shapeContext, context.stylesReader().customStyles("list").values(), ContentDotXml | StylesDotXml, styleManager); addDefaultParagraphStyle(shapeContext, context.stylesReader().defaultStyle("paragraph"), context.defaultStylesReader().defaultStyle("paragraph"), styleManager); // adding all the styles in order of dependency; automatic styles can have a parent in the named styles, so load the named styles first. // add office:styles from styles.xml to paragraphContentDotXmlStyles, paragraphStylesDotXmlStyles and styleManager // now all styles referencable from the body in content.xml is in paragraphContentDotXmlStyles addParagraphStyles(shapeContext, context.stylesReader().customStyles("paragraph").values(), ContentDotXml | StylesDotXml, styleManager); // add office:automatic-styles in styles.xml to paragraphStylesDotXmlStyles addParagraphStyles(shapeContext, context.stylesReader().autoStyles("paragraph", true).values(), StylesDotXml); // add office:automatic-styles in content.xml to paragraphContentDotXmlStyles addParagraphStyles(shapeContext, context.stylesReader().autoStyles("paragraph").values(), ContentDotXml); addTableStyles(context, context.stylesReader().autoStyles("table").values(), ContentDotXml); addTableStyles(context, context.stylesReader().autoStyles("table", true).values(), StylesDotXml); addTableStyles(context, context.stylesReader().customStyles("table").values(), ContentDotXml | StylesDotXml, styleManager); addTableColumnStyles(context, context.stylesReader().autoStyles("table-column").values(), ContentDotXml); addTableColumnStyles(context, context.stylesReader().autoStyles("table-column", true).values(), StylesDotXml); addTableColumnStyles(context, context.stylesReader().customStyles("table-column").values(), ContentDotXml | StylesDotXml, styleManager); addTableRowStyles(context, context.stylesReader().autoStyles("table-row").values(), ContentDotXml); addTableRowStyles(context, context.stylesReader().autoStyles("table-row", true).values(), StylesDotXml); addTableRowStyles(context, context.stylesReader().customStyles("table-row").values(), ContentDotXml | StylesDotXml, styleManager); addTableCellStyles(shapeContext, context.stylesReader().autoStyles("table-cell").values(), ContentDotXml); addTableCellStyles(shapeContext, context.stylesReader().autoStyles("table-cell", true).values(), StylesDotXml); addTableCellStyles(shapeContext, context.stylesReader().customStyles("table-cell").values(), ContentDotXml | StylesDotXml, styleManager); addSectionStyles(context, context.stylesReader().autoStyles("section").values(), ContentDotXml); addSectionStyles(context, context.stylesReader().autoStyles("section", true).values(), StylesDotXml); addSectionStyles(context, context.stylesReader().customStyles("section").values(), ContentDotXml | StylesDotXml, styleManager); addOutlineStyle(shapeContext, styleManager); addNotesConfiguration(shapeContext, styleManager); addTableTemplate(shapeContext, styleManager); debugText << "content.xml: paragraph styles" << d->paragraphContentDotXmlStyles.count() << "character styles" << d->characterContentDotXmlStyles.count(); debugText << "styles.xml: paragraph styles" << d->paragraphStylesDotXmlStyles.count() << "character styles" << d->characterStylesDotXmlStyles.count(); } void KoTextSharedLoadingData::addParagraphStyles(KoShapeLoadingContext &context, const QList &styleElements, int styleTypes, KoStyleManager *styleManager) { QList > paragraphStyles(loadParagraphStyles(context, styleElements, styleTypes, styleManager)); QList >::iterator it(paragraphStyles.begin()); for (; it != paragraphStyles.end(); ++it) { if (styleTypes & ContentDotXml) { d->paragraphContentDotXmlStyles.insert(it->first, it->second); } if (styleTypes & StylesDotXml) { d->paragraphStylesDotXmlStyles.insert(it->first, it->second); } } } QList > KoTextSharedLoadingData::loadParagraphStyles(KoShapeLoadingContext &context, const QList &styleElements, int styleTypes, KoStyleManager *styleManager) { QList > paragraphStyles; QHash nextStyles; QHash parentStyles; Q_FOREACH (KoXmlElement *styleElem, styleElements) { Q_ASSERT(styleElem); Q_ASSERT(!styleElem->isNull()); QString name = styleElem->attributeNS(KoXmlNS::style, "name", QString()); KoParagraphStyle *parastyle = new KoParagraphStyle(); parastyle->loadOdf(styleElem, context); QString listStyleName = styleElem->attributeNS(KoXmlNS::style, "list-style-name", QString()); KoListStyle *list = listStyle(listStyleName, styleTypes & StylesDotXml); if (list) { KoListStyle *newListStyle = new KoListStyle(parastyle); newListStyle->copyProperties(list); parastyle->setListStyle(newListStyle); } paragraphStyles.append(QPair(name, parastyle)); d->namedParagraphStyles.insert(name, parastyle); if (styleElem->hasAttributeNS(KoXmlNS::style, "next-style-name")) nextStyles.insert(parastyle, styleElem->attributeNS(KoXmlNS::style, "next-style-name")); if (styleElem->hasAttributeNS(KoXmlNS::style, "parent-style-name")) parentStyles.insert(parastyle, styleElem->attributeNS(KoXmlNS::style, "parent-style-name")); // TODO check if it a know style set the styleid so that the custom styles are kept during copy and paste - // in case styles are not added to the style manager they have to be deleted after loading to avoid leaking memeory + // in case styles are not added to the style manager they have to be deleted after loading to avoid leaking memory if (styleManager) { styleManager->add(parastyle); } else { d->paragraphStylesToDelete.append(parastyle); } parastyle->setDefaultStyle(d->defaultParagraphStyle); } // second pass; resolve all the 'next-style's and parent-style's. // TODO iterate via values foreach (KoParagraphStyle *style, nextStyles.keys()) { KoParagraphStyle *next = d->namedParagraphStyles.value(nextStyles.value(style)); if (next && next->styleId() >= 0) style->setNextStyle(next->styleId()); } foreach (KoParagraphStyle *style, parentStyles.keys()) { KoParagraphStyle *parent = d->namedParagraphStyles.value(parentStyles.value(style)); if (parent) style->setParentStyle(parent); } return paragraphStyles; } void KoTextSharedLoadingData::addCharacterStyles(KoShapeLoadingContext &context, const QList &styleElements, int styleTypes, KoStyleManager *styleManager) { QList characterStyles(loadCharacterStyles(context, styleElements)); foreach (const OdfCharStyle &odfStyle, characterStyles) { if (styleTypes & ContentDotXml) { d->characterContentDotXmlStyles.insert(odfStyle.odfName, odfStyle.style); } if (styleTypes & StylesDotXml) { d->characterStylesDotXmlStyles.insert(odfStyle.odfName, odfStyle.style); } if (styleManager) { styleManager->add(odfStyle.style); } else { d->characterStylesToDelete.append(odfStyle.style); } } // now that all name styles have been added we resolve parent relation ships foreach (const OdfCharStyle &odfStyle, characterStyles) { KoCharacterStyle *parent = 0; if (!odfStyle.parentStyle.isEmpty()) { parent = characterStyle(odfStyle.parentStyle, false); if (!parent) parent = characterStyle(odfStyle.parentStyle, true); // try harder odfStyle.style->setParentStyle(parent); } if (!styleManager) { if (parent) { // an auto style with a parent. // let's set the styleId of that one on the auto-style too. // this will have the effect that wherever the autostyle is applied, it will // cause the parent style-id to be applied. So we don't loose this info. odfStyle.style->setStyleId(parent->styleId()); } } odfStyle.style->setDefaultStyle(d->defaultCharacterStyle); } } QList KoTextSharedLoadingData::loadCharacterStyles(KoShapeLoadingContext &shapeContext, const QList &styleElements) { QList characterStyles; Q_FOREACH (KoXmlElement *styleElem, styleElements) { Q_ASSERT(styleElem); Q_ASSERT(!styleElem->isNull()); OdfCharStyle answer; answer.odfName = styleElem->attributeNS(KoXmlNS::style, "name", QString()); answer.parentStyle = styleElem->attributeNS(KoXmlNS::style, "parent-style-name"); answer.style = new KoCharacterStyle(); answer.style->loadOdf(styleElem, shapeContext); characterStyles.append(answer); } return characterStyles; } void KoTextSharedLoadingData::addListStyles(KoShapeLoadingContext &context, const QList &styleElements, int styleTypes, KoStyleManager *styleManager) { QList > listStyles(loadListStyles(context, styleElements)); QList >::iterator it(listStyles.begin()); for (; it != listStyles.end(); ++it) { if (styleTypes & ContentDotXml) { d->listContentDotXmlStyles.insert(it->first, it->second); } if (styleTypes & StylesDotXml) { d->listStylesDotXmlStyles.insert(it->first, it->second); } // TODO check if it a know style set the styleid so that the custom styles are kept during copy and paste // in case styles are not added to the style manager they have to be deleted after loading to avoid leaking memory if (styleManager) { styleManager->add(it->second); } else { d->listStylesToDelete.append(it->second); } } } QList > KoTextSharedLoadingData::loadListStyles(KoShapeLoadingContext &context, const QList &styleElements) { QList > listStyles; Q_FOREACH (KoXmlElement *styleElem, styleElements) { Q_ASSERT(styleElem); Q_ASSERT(!styleElem->isNull()); QString name = styleElem->attributeNS(KoXmlNS::style, "name", QString()); KoListStyle *liststyle = new KoListStyle(); liststyle->loadOdf(context, *styleElem); listStyles.append(QPair(name, liststyle)); } return listStyles; } void KoTextSharedLoadingData::addTableStyles(KoOdfLoadingContext &context, const QList &styleElements, int styleTypes, KoStyleManager *styleManager) { QList > tableStyles(loadTableStyles(context, styleElements)); QList >::iterator it(tableStyles.begin()); for (; it != tableStyles.end(); ++it) { if (styleTypes & ContentDotXml) { d->tableContentDotXmlStyles.insert(it->first, it->second); } if (styleTypes & StylesDotXml) { d->tableStylesDotXmlStyles.insert(it->first, it->second); } // TODO check if it a know style set the styleid so that the custom styles are kept during copy and paste - // in case styles are not added to the style manager they have to be deleted after loading to avoid leaking memeory + // in case styles are not added to the style manager they have to be deleted after loading to avoid leaking memory if (styleManager) { styleManager->add(it->second); } else { d->tableStylesToDelete.append(it->second); } } } QList > KoTextSharedLoadingData::loadTableStyles(KoOdfLoadingContext &context, const QList &styleElements) { QList > tableStyles; Q_FOREACH (KoXmlElement *styleElem, styleElements) { Q_ASSERT(styleElem); Q_ASSERT(!styleElem->isNull()); QString name = styleElem->attributeNS(KoXmlNS::style, "name", QString()); // nah don't think this is it: context.fillStyleStack(*styleElem, KoXmlNS::style, "style-name", "table"); KoTableStyle *tablestyle = new KoTableStyle(); tablestyle->loadOdf(styleElem, context); tableStyles.append(QPair(name, tablestyle)); } return tableStyles; } void KoTextSharedLoadingData::addTableColumnStyles(KoOdfLoadingContext &context, const QList &styleElements, int styleTypes, KoStyleManager *styleManager) { QList > tableColumnStyles(loadTableColumnStyles(context, styleElements)); QList >::iterator it(tableColumnStyles.begin()); for (; it != tableColumnStyles.end(); ++it) { if (styleTypes & ContentDotXml) { d->tableColumnContentDotXmlStyles.insert(it->first, it->second); } if (styleTypes & StylesDotXml) { d->tableColumnStylesDotXmlStyles.insert(it->first, it->second); } // TODO check if it a know style set the styleid so that the custom styles are kept during copy and paste - // in case styles are not added to the style manager they have to be deleted after loading to avoid leaking memeory + // in case styles are not added to the style manager they have to be deleted after loading to avoid leaking memory if (styleManager) { styleManager->add(it->second); } else { d->tableColumnStylesToDelete.append(it->second); } } } QList > KoTextSharedLoadingData::loadTableColumnStyles(KoOdfLoadingContext &context, const QList &styleElements) { QList > tableColumnStyles; Q_FOREACH (KoXmlElement *styleElem, styleElements) { Q_ASSERT(styleElem); Q_ASSERT(!styleElem->isNull()); QString name = styleElem->attributeNS(KoXmlNS::style, "name", QString()); // nah don't think this is it: context.fillStyleStack(*styleElem, KoXmlNS::style, "style-name", "table"); KoTableColumnStyle *tablecolumnstyle = new KoTableColumnStyle(); tablecolumnstyle->loadOdf(styleElem, context); tableColumnStyles.append(QPair(name, tablecolumnstyle)); } return tableColumnStyles; } void KoTextSharedLoadingData::addTableRowStyles(KoOdfLoadingContext &context, const QList &styleElements, int styleTypes, KoStyleManager *styleManager) { QList > tableRowStyles(loadTableRowStyles(context, styleElements)); QList >::iterator it(tableRowStyles.begin()); for (; it != tableRowStyles.end(); ++it) { if (styleTypes & ContentDotXml) { d->tableRowContentDotXmlStyles.insert(it->first, it->second); } if (styleTypes & StylesDotXml) { d->tableRowStylesDotXmlStyles.insert(it->first, it->second); } // TODO check if it a know style set the styleid so that the custom styles are kept during copy and paste - // in case styles are not added to the style manager they have to be deleted after loading to avoid leaking memeory + // in case styles are not added to the style manager they have to be deleted after loading to avoid leaking memory if (styleManager) { styleManager->add(it->second); } else { d->tableRowStylesToDelete.append(it->second); } } } QList > KoTextSharedLoadingData::loadTableRowStyles(KoOdfLoadingContext &context, const QList &styleElements) { QList > tableRowStyles; Q_FOREACH (KoXmlElement *styleElem, styleElements) { Q_ASSERT(styleElem); Q_ASSERT(!styleElem->isNull()); QString name = styleElem->attributeNS(KoXmlNS::style, "name", QString()); // nah don't think this is it: context.fillStyleStack(*styleElem, KoXmlNS::style, "style-name", "table"); KoTableRowStyle *tablerowstyle = new KoTableRowStyle(); tablerowstyle->loadOdf(styleElem, context); tableRowStyles.append(QPair(name, tablerowstyle)); } return tableRowStyles; } void KoTextSharedLoadingData::addTableCellStyles(KoShapeLoadingContext &context, const QList &styleElements, int styleTypes, KoStyleManager *styleManager) { QList > tableCellStyles(loadTableCellStyles(context, styleElements)); QList >::iterator it(tableCellStyles.begin()); for (; it != tableCellStyles.end(); ++it) { if (styleTypes & ContentDotXml) { d->tableCellContentDotXmlStyles.insert(it->first, it->second); } if (styleTypes & StylesDotXml) { d->tableCellStylesDotXmlStyles.insert(it->first, it->second); } // TODO check if it a know style set the styleid so that the custom styles are kept during copy and paste - // in case styles are not added to the style manager they have to be deleted after loading to avoid leaking memeory + // in case styles are not added to the style manager they have to be deleted after loading to avoid leaking memory if (styleManager) { styleManager->add(it->second); } else { d->tableCellStylesToDelete.append(it->second); } } } QList > KoTextSharedLoadingData::loadTableCellStyles(KoShapeLoadingContext &context, const QList &styleElements) { QList > tableCellStyles; Q_FOREACH (KoXmlElement *styleElem, styleElements) { Q_ASSERT(styleElem); Q_ASSERT(!styleElem->isNull()); QString name = styleElem->attributeNS(KoXmlNS::style, "name", QString()); // nah don't think this is it: context.fillStyleStack(*styleElem, KoXmlNS::style, "style-name", "table"); KoTableCellStyle *tablecellstyle = new KoTableCellStyle(); tablecellstyle->loadOdf(styleElem, context); tableCellStyles.append(QPair(name, tablecellstyle)); } return tableCellStyles; } void KoTextSharedLoadingData::addSectionStyles(KoOdfLoadingContext &context, const QList &styleElements, int styleTypes, KoStyleManager *styleManager) { QList > sectionStyles(loadSectionStyles(context, styleElements)); QList >::iterator it(sectionStyles.begin()); for (; it != sectionStyles.end(); ++it) { if (styleTypes & ContentDotXml) { d->sectionContentDotXmlStyles.insert(it->first, it->second); } if (styleTypes & StylesDotXml) { d->sectionStylesDotXmlStyles.insert(it->first, it->second); } // TODO check if it a know style set the styleid so that the custom styles are kept during copy and paste - // in case styles are not added to the style manager they have to be deleted after loading to avoid leaking memeory + // in case styles are not added to the style manager they have to be deleted after loading to avoid leaking memory if (styleManager) { styleManager->add(it->second); } else { d->sectionStylesToDelete.append(it->second); } } } QList > KoTextSharedLoadingData::loadSectionStyles(KoOdfLoadingContext &context, const QList &styleElements) { QList > sectionStyles; Q_FOREACH (KoXmlElement *styleElem, styleElements) { Q_ASSERT(styleElem); Q_ASSERT(!styleElem->isNull()); QString name = styleElem->attributeNS(KoXmlNS::style, "name", QString()); // nah don't think this is it: context.fillStyleStack(*styleElem, KoXmlNS::style, "style-name", "table"); KoSectionStyle *sectionstyle = new KoSectionStyle(); sectionstyle->loadOdf(styleElem, context); sectionStyles.append(QPair(name, sectionstyle)); } return sectionStyles; } void KoTextSharedLoadingData::addOutlineStyle(KoShapeLoadingContext &context, KoStyleManager *styleManager) { // outline-styles used e.g. for headers KoXmlElement outlineStyleElem = KoXml::namedItemNS(context.odfLoadingContext().stylesReader().officeStyle(), KoXmlNS::text, "outline-style"); if (styleManager && outlineStyleElem.isElement()) { KoListStyle *outlineStyle = new KoListStyle(); outlineStyle->loadOdf(context, outlineStyleElem); styleManager->setOutlineStyle(outlineStyle); // style manager owns it now and will take care of deleting it. } } KoParagraphStyle *KoTextSharedLoadingData::paragraphStyle(const QString &name, bool stylesDotXml) const { return stylesDotXml ? d->paragraphStylesDotXmlStyles.value(name) : d->paragraphContentDotXmlStyles.value(name); } QList KoTextSharedLoadingData::paragraphStyles(bool stylesDotXml) const { return stylesDotXml ? d->paragraphStylesDotXmlStyles.values() : d->paragraphContentDotXmlStyles.values(); } KoCharacterStyle *KoTextSharedLoadingData::characterStyle(const QString &name, bool stylesDotXml) const { return stylesDotXml ? d->characterStylesDotXmlStyles.value(name) : d->characterContentDotXmlStyles.value(name); } QList KoTextSharedLoadingData::characterStyles(bool stylesDotXml) const { return stylesDotXml ? d->characterStylesDotXmlStyles.values() : d->characterContentDotXmlStyles.values(); } KoListStyle *KoTextSharedLoadingData::listStyle(const QString &name, bool stylesDotXml) const { return stylesDotXml ? d->listStylesDotXmlStyles.value(name) : d->listContentDotXmlStyles.value(name); } KoTableStyle *KoTextSharedLoadingData::tableStyle(const QString &name, bool stylesDotXml) const { return stylesDotXml ? d->tableStylesDotXmlStyles.value(name) : d->tableContentDotXmlStyles.value(name); } KoTableColumnStyle *KoTextSharedLoadingData::tableColumnStyle(const QString &name, bool stylesDotXml) const { return stylesDotXml ? d->tableColumnStylesDotXmlStyles.value(name) : d->tableColumnContentDotXmlStyles.value(name); } KoTableRowStyle *KoTextSharedLoadingData::tableRowStyle(const QString &name, bool stylesDotXml) const { return stylesDotXml ? d->tableRowStylesDotXmlStyles.value(name) : d->tableRowContentDotXmlStyles.value(name); } KoTableCellStyle *KoTextSharedLoadingData::tableCellStyle(const QString &name, bool stylesDotXml) const { return stylesDotXml ? d->tableCellStylesDotXmlStyles.value(name) : d->tableCellContentDotXmlStyles.value(name); } KoSectionStyle *KoTextSharedLoadingData::sectionStyle(const QString &name, bool stylesDotXml) const { return stylesDotXml ? d->sectionStylesDotXmlStyles.value(name) : d->sectionContentDotXmlStyles.value(name); } KoOdfBibliographyConfiguration KoTextSharedLoadingData::bibliographyConfiguration() const { return d->bibliographyConfiguration; } void KoTextSharedLoadingData::shapeInserted(KoShape *shape, const KoXmlElement &element, KoShapeLoadingContext &/*context*/) { d->insertedShapes.append(shape); Q_UNUSED(element); } QList KoTextSharedLoadingData::insertedShapes() const { return d->insertedShapes; } void KoTextSharedLoadingData::addNotesConfiguration(KoShapeLoadingContext &context, KoStyleManager *styleManager) { KoOdfNotesConfiguration *footnotesConfiguration = new KoOdfNotesConfiguration( context.odfLoadingContext().stylesReader().globalNotesConfiguration(KoOdfNotesConfiguration::Footnote)); KoOdfNotesConfiguration *endnotesConfiguration = new KoOdfNotesConfiguration( context.odfLoadingContext().stylesReader().globalNotesConfiguration(KoOdfNotesConfiguration::Endnote)); footnotesConfiguration->setCitationBodyTextStyle(d->characterStylesDotXmlStyles.value(footnotesConfiguration->citationBodyTextStyleName())); footnotesConfiguration->setCitationTextStyle(d->characterStylesDotXmlStyles.value(footnotesConfiguration->citationTextStyleName())); footnotesConfiguration->setDefaultNoteParagraphStyle(d->paragraphStylesDotXmlStyles.value(footnotesConfiguration->defaultNoteParagraphStyleName())); endnotesConfiguration->setCitationBodyTextStyle(d->characterStylesDotXmlStyles.value(endnotesConfiguration->citationBodyTextStyleName())); endnotesConfiguration->setCitationTextStyle(d->characterStylesDotXmlStyles.value(endnotesConfiguration->citationTextStyleName())); endnotesConfiguration->setDefaultNoteParagraphStyle(d->paragraphStylesDotXmlStyles.value(endnotesConfiguration->defaultNoteParagraphStyleName())); styleManager->setNotesConfiguration(footnotesConfiguration); styleManager->setNotesConfiguration(endnotesConfiguration); } void KoTextSharedLoadingData::addBibliographyConfiguration(KoShapeLoadingContext &context) { d->bibliographyConfiguration = context.odfLoadingContext().stylesReader().globalBibliographyConfiguration(); } void KoTextSharedLoadingData::addTableTemplate(KoShapeLoadingContext &context, KoStyleManager *styleManager) { QList > tableTemplates(loadTableTemplates(context)); QList >::iterator it(tableTemplates.begin()); for (; it != tableTemplates.end(); ++it) { d->tableTemplates.insert(it->first, it->second); - // in case templates are not added to the style manager they have to be deleted after loading to avoid leaking memeory + // in case templates are not added to the style manager they have to be deleted after loading to avoid leaking memory if (styleManager) { styleManager->add(it->second); } else { d->tableTemplatesToDelete.append(it->second); } } } QList > KoTextSharedLoadingData::loadTableTemplates(KoShapeLoadingContext &context) { QList > tableTemplates; Q_FOREACH (KoXmlElement *styleElem, context.odfLoadingContext().stylesReader().tableTemplates()) { Q_ASSERT(styleElem); Q_ASSERT(!styleElem->isNull()); KoTextTableTemplate *tableTemplate = new KoTextTableTemplate(); tableTemplate->loadOdf(styleElem, context); tableTemplates.append(QPair(tableTemplate->name(), tableTemplate)); } return tableTemplates; } diff --git a/plugins/flake/textshape/kotext/styles/KoListLevelProperties.h b/plugins/flake/textshape/kotext/styles/KoListLevelProperties.h index 3442a8d241..d333bbee96 100644 --- a/plugins/flake/textshape/kotext/styles/KoListLevelProperties.h +++ b/plugins/flake/textshape/kotext/styles/KoListLevelProperties.h @@ -1,217 +1,217 @@ /* This file is part of the KDE project * Copyright (C) 2006-2007 Thomas Zander * Copyright (C) 2010 Nandita Suri * Copyright (C) 2011-2012 Gopalakrishna Bhat A * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOLISTLEVELPROPERTIES_H #define KOLISTLEVELPROPERTIES_H #include "KoListStyle.h" #include class KoCharacterStyle; class KoShapeLoadingContext; class KoShapeSavingContext; class KoXmlWriter; class KoImageData; class QTextList; /** * Properties per list level. */ class KRITATEXT_EXPORT KoListLevelProperties : public QObject { Q_OBJECT public: /// Constructor explicit KoListLevelProperties(); /// Copy constructor KoListLevelProperties(const KoListLevelProperties &other); /// Destructor ~KoListLevelProperties() override; /// each style has a unique ID (non persistent) given out by the styleManager int styleId() const; /// each style has a unique ID (non persistent) given out by the styleManager void setStyleId(int id); /// set the style to be used for this list-level. void setStyle(KoListStyle::Style style); /// return the used style KoListStyle::Style style() const; /// set the string that will be shown before the counter in the list label void setListItemPrefix(const QString &prefix); /// return the string that will be shown before the counter in the list label QString listItemPrefix() const; /// set the string that will be shown after the counter in the list label void setListItemSuffix(const QString &suffix); /// return the string that will be shown after the counter in the list label QString listItemSuffix() const; /// set the index of the first value of this whole list. void setStartValue(int value); /// return the index of the first value of this whole list. int startValue() const; /// set the list level which is how deep the counter is nested below other lists (should be >=1) void setLevel(int level); /// return the list level which is how deep the counter is nested below other lists int level() const; /// set the amount of levels that will be shown in list items of this list. void setDisplayLevel(int level); /// return the amount of levels that will be shown in list items of this list. int displayLevel() const; /// set the styleId of the KoCharacterStyle to be used to layout the listitem void setCharacterStyleId(int id); /// return the styleId of the KoCharacterStyle to be used to layout the listitem int characterStyleId() const; /// set the style for the bullet or the number of the list void setCharacterProperties(QSharedPointer style); /// return the KoCharacterStyle for the bullet or the number of the list QSharedPointer characterProperties() const; /// set the character to be used as the counter of the listitem void setBulletCharacter(QChar character); /// return the character to be used as the counter of the listitem QChar bulletCharacter() const; /// set the size, in percent, of the bullet counter relative to the fontsize of the counter void setRelativeBulletSize(int percent); /// return the size, in percent, of the bullet counter relative to the fontsize of the counter int relativeBulletSize() const; /// set how the list label should be aligned in the width this list reserves for the listitems void setAlignment(Qt::Alignment align); /// return how the list label should be aligned in the width this list reserves for the listitems Qt::Alignment alignment() const; /// set the minimum width (in pt) of the list label for all items in this list void setMinimumWidth(qreal width); /// return the minimum width (in pt) of the list label for all items in this list qreal minimumWidth() const; /// set the width (in pt) of the image bullet void setWidth(qreal width); /// return the width (in pt) of the image bullet qreal width() const; /// set the height (in pt) of the image bullet void setHeight(qreal height); /// return the height (in pt) of the image bullet qreal height() const; /// set the bullet image key (as from the KoImageData) void setBulletImage(KoImageData *imageData); /// return the bullet image that is used in the list(as KoImageData) KoImageData *bulletImage() const; /// set the listId used by all list-styles that together make 1 user defined list in an ODF file. void setListId(KoListStyle::ListIdType listId); /// return the listId used by all list-styles that together make 1 user defined list in an ODF file. KoListStyle::ListIdType listId() const; /** * For alpha-based lists numbers above the 'z' will increase the value of all characters at the same time. * If true; we get the sequence 'aa', 'bb', 'cc'. If false; 'aa', 'ab', 'ac'. * @return if letterSynchronization should be applied. */ bool letterSynchronization() const; /** * For alpha-based lists numbers above the 'z' will increase the value of all characters at the same time. * If true; we get the sequence 'aa', 'bb', 'cc'. If false; 'aa', 'ab', 'ac'. * @param on if letterSynchronization should be applied. */ void setLetterSynchronization(bool on); /// sets the indentation of paragraph void setIndent(qreal value); /// returns the indentation of paragraphs qreal indent() const; /// sets the minimum distance between the counter and the text void setMinimumDistance(qreal value); /// returns the minimum distance between the counter and text qreal minimumDistance() const; /// sets the margin of the list - void setMargin(qreal vlaue); + void setMargin(qreal value); /// returns the margin of the list qreal margin() const; /// sets the text indent of the list item void setTextIndent(qreal value); /// returns the text indent of the list item qreal textIndent() const; /// set the item that follows the label; this is used if alignmentMode() is true void setLabelFollowedBy(KoListStyle::ListLabelFollowedBy value); /// returns the item that follows the label; this is used if alignmentMode() is true KoListStyle::ListLabelFollowedBy labelFollowedBy() const; /// sets the value of tab stop that follows the label, it is used only if ListLabelFollowedBy is ListTab void setTabStopPosition(qreal value); /// returns the value of tab stop that follows the label, it is used only if ListLabelFollowedBy is ListTab qreal tabStopPosition() const; /// sets the alignment mode of the list isLabelAlignmentMode=true if ist-level-position-and-space-mode=label-alignment void setAlignmentMode(bool isLabelAlignmentMode); /// return the alignment mode of the list isLabelAlignmentMode=true if ist-level-position-and-space-mode=label-alignment bool alignmentMode() const; void setOutlineList(bool isOutline); bool isOutlineList() const; bool operator==(const KoListLevelProperties &other) const; bool operator!=(const KoListLevelProperties &other) const; KoListLevelProperties & operator=(const KoListLevelProperties &other); /** * Create a KoListLevelProperties object from a QTextList instance. */ static KoListLevelProperties fromTextList(QTextList *list); /** * Apply this style to a QTextListFormat by copying all properties from this style * to the target list format. */ void applyStyle(QTextListFormat &format) const; /** * Load the properties from the \p style using the OpenDocument format. */ void loadOdf(KoShapeLoadingContext& scontext, const KoXmlElement& style); /** * Save the properties of the style using the OpenDocument format */ void saveOdf(KoXmlWriter *writer, KoShapeSavingContext &context) const; public Q_SLOTS: void onStyleChanged(int key); Q_SIGNALS: void styleChanged(int key); private: void setProperty(int key, const QVariant &value); int propertyInt(int key) const; uint propertyUInt(int key) const; qulonglong propertyULongLong(int key) const; bool propertyBoolean(int key) const; qreal propertyDouble(int key) const; QString propertyString(int key) const; QColor propertyColor(int key) const; QVariant property(int key) const; class Private; Private * const d; }; #endif diff --git a/plugins/flake/textshape/textlayout/ListItemsHelper.cpp b/plugins/flake/textshape/textlayout/ListItemsHelper.cpp index 36f764109d..aa30260f90 100644 --- a/plugins/flake/textshape/textlayout/ListItemsHelper.cpp +++ b/plugins/flake/textshape/textlayout/ListItemsHelper.cpp @@ -1,503 +1,503 @@ /* This file is part of the KDE project * Copyright (C) 2006-2007, 2010 Thomas Zander * Copyright (C) 2008 Girish Ramakrishnan * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "ListItemsHelper.h" #include #include #include #include #include #include #include using namespace Lists; QString Lists::intToRoman(int n) { static const QString RNUnits[] = {"", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix"}; static const QString RNTens[] = {"", "x", "xx", "xxx", "xl", "l", "lx", "lxx", "lxxx", "xc"}; static const QString RNHundreds[] = {"", "c", "cc", "ccc", "cd", "d", "dc", "dcc", "dccc", "cm"}; static const QString RNThousands[] = {"", "m", "mm", "mmm", "mmmm", "mmmmm", "mmmmmm", "mmmmmmm", "mmmmmmmm", "mmmmmmmmm"}; if (n <= 0) { warnTextLayout << "intToRoman called with negative number: n=" << n; return QString::number(n); } return RNThousands[(n / 1000)] + RNHundreds[(n / 100) % 10 ] + RNTens[(n / 10) % 10 ] + RNUnits[(n) % 10 ]; } QString Lists::intToAlpha(int n, Capitalisation caps, bool letterSynchronization) { const char offset = caps == Uppercase ? 'A' : 'a'; QString answer; if (letterSynchronization) { int digits = 1; for (; n > 26; n -= 26) digits += 1; for (int i = 0; i < digits; i++) answer.prepend(QChar(offset + n - 1)); return answer; } else { char bottomDigit; while (n > 26) { bottomDigit = (n - 1) % 26; n = (n - 1) / 26; answer.prepend(QChar(offset + bottomDigit)); } } answer.prepend(QChar(offset + n - 1)); return answer; } QString Lists::intToScript(int n, KoListStyle::Style type) { // 10-base static const int bengali = 0x9e6; static const int gujarati = 0xae6; static const int gurumukhi = 0xa66; static const int kannada = 0xce6; static const int malayalam = 0xd66; static const int oriya = 0xb66; static const int tamil = 0x0be6; static const int telugu = 0xc66; static const int tibetan = 0xf20; static const int thai = 0xe50; int offset; switch (type) { case KoListStyle::Bengali: offset = bengali; break; case KoListStyle::Gujarati: offset = gujarati; break; case KoListStyle::Gurumukhi: offset = gurumukhi; break; case KoListStyle::Kannada: offset = kannada; break; case KoListStyle::Malayalam: offset = malayalam; break; case KoListStyle::Oriya: offset = oriya; break; case KoListStyle::Tamil: offset = tamil; break; case KoListStyle::Telugu: offset = telugu; break; case KoListStyle::Tibetan: offset = tibetan; break; case KoListStyle::Thai: offset = thai; break; default: return QString::number(n); } QString answer; while (n > 0) { answer.prepend(QChar(offset + n % 10)); n = n / 10; } return answer; } QString Lists::intToScriptList(int n, KoListStyle::Style type) { // 1 time Sequences // note; the leading X is to make these 1 based. static const char* const Abjad[] = { "أ", "ب", "ج", "د", "ﻫ", "و", "ز", "ح", "ط", "ي", "ك", "ل", "م", "ن", "س", "ع", "ف", "ص", "ق", "ر", "ش", "ت", "ث", "خ", "ذ", "ض", "ظ", "غ" }; static const char* const Abjad2[] = { "ﺃ", "ﺏ", "ﺝ", "ﺩ", "ﻫ", "ﻭ", "ﺯ", "ﺡ", "ﻁ", "ﻱ", "ﻙ", "ﻝ", "ﻡ", "ﻥ", "ﺹ", "ﻉ", "ﻑ", "ﺽ", "ﻕ", "ﺭ", "ﺱ", "ﺕ", "ﺙ", "ﺥ", "ﺫ", "ﻅ", "ﻍ", "ﺵ" }; static const char* const ArabicAlphabet[] = {"ا", "ب", "ت", "ث", "ج", "ح", "خ", "د", "ذ", "ر", "ز", "س", "ش", "ص", "ض", "ط", "ظ", "ع", "غ", "ف", "ق", "ك", "ل", "م", "ن", "ه", "و", "ي" }; /* // see this page for the 10, 100, 1000 etc http://en.wikipedia.org/wiki/Chinese_numerals static const char* const chinese1[] = { '零','壹','貳','叄','肆','伍','陸','柒','捌','玖' }; static const char* const chinese2[] = { '〇','一','二','三','四','五','六','七','八','九' }; TODO: http://en.wikipedia.org/wiki/Korean_numerals http://en.wikipedia.org/wiki/Japanese_numerals 'http://en.wikipedia.org/wiki/Hebrew_numerals' 'http://en.wikipedia.org/wiki/Armenian_numerals' 'http://en.wikipedia.org/wiki/Greek_numerals' 'http://en.wikipedia.org/wiki/Cyrillic_numerals' 'http://en.wikipedia.org/wiki/Sanskrit_numerals' 'http://en.wikipedia.org/wiki/Ge%27ez_alphabet#Numerals' 'http://en.wikipedia.org/wiki/Abjad_numerals' */ switch (type) { case KoListStyle::Abjad: if (n > 22) return "*"; return QString::fromUtf8(Abjad[n-1]); case KoListStyle::AbjadMinor: if (n > 22) return "*"; return QString::fromUtf8(Abjad2[n-1]); case KoListStyle::ArabicAlphabet: if (n > 28) return "*"; return QString::fromUtf8(ArabicAlphabet[n-1]); default: return QString::number(n); } } QString Lists::intToNumberingStyle(int index, KoListStyle::Style listStyle, bool letterSynchronization) { QString counterText; switch(listStyle) { case KoListStyle::DecimalItem: counterText = QString::number(index); break; case KoListStyle::AlphaLowerItem: counterText = intToAlpha(index, Lowercase, letterSynchronization); break; case KoListStyle::UpperAlphaItem: counterText = intToAlpha(index, Uppercase, letterSynchronization); break; case KoListStyle::RomanLowerItem: counterText = intToRoman(index); break; case KoListStyle::UpperRomanItem: counterText = intToRoman(index).toUpper(); break; case KoListStyle::Bengali: case KoListStyle::Gujarati: case KoListStyle::Gurumukhi: case KoListStyle::Kannada: case KoListStyle::Malayalam: case KoListStyle::Oriya: case KoListStyle::Tamil: case KoListStyle::Telugu: case KoListStyle::Tibetan: case KoListStyle::Thai: counterText = intToScript(index, listStyle); break; case KoListStyle::Abjad: case KoListStyle::ArabicAlphabet: case KoListStyle::AbjadMinor: counterText = intToScriptList(index, listStyle); break; default: counterText = QString::number(index); } return counterText; } QList Lists::genericListStyleItems() { QList answer; answer.append(ListStyleItem(i18nc("Text list-style", "None"), KoListStyle::None)); answer.append(ListStyleItem(i18n("Small Bullet"), KoListStyle::Bullet)); answer.append(ListStyleItem(i18n("Circle Bullet"), KoListStyle::CircleItem)); answer.append(ListStyleItem(i18n("Square Bullet"), KoListStyle::SquareItem)); answer.append(ListStyleItem(i18n("Rhombus Bullet"), KoListStyle::RhombusItem)); answer.append(ListStyleItem(i18n("Check Mark Bullet"), KoListStyle::HeavyCheckMarkItem)); answer.append(ListStyleItem(i18n("Rightwards Arrow Bullet"), KoListStyle::RightArrowItem)); answer.append(ListStyleItem(i18n("Arabic"), KoListStyle::DecimalItem)); answer.append(ListStyleItem(i18n("Lower Alphabetical"), KoListStyle::AlphaLowerItem)); answer.append(ListStyleItem(i18n("Upper Alphabetical"), KoListStyle::UpperAlphaItem)); answer.append(ListStyleItem(i18n("Lower Roman"), KoListStyle::RomanLowerItem)); answer.append(ListStyleItem(i18n("Upper Roman"), KoListStyle::UpperRomanItem)); return answer; } QList Lists::otherListStyleItems() { QList answer; answer.append(ListStyleItem(i18n("Large Bullet"), KoListStyle::BlackCircle)); answer.append(ListStyleItem(i18n("Ballot X Bullet"), KoListStyle::BallotXItem)); answer.append(ListStyleItem(i18n("Rightwards Arrow Head Bullet"), KoListStyle::RightArrowHeadItem)); answer.append(ListStyleItem(i18n("Bengali"), KoListStyle::Bengali)); answer.append(ListStyleItem(i18n("Gujarati"), KoListStyle::Gujarati)); answer.append(ListStyleItem(i18n("Gurumukhi"), KoListStyle::Gurumukhi)); answer.append(ListStyleItem(i18n("Kannada"), KoListStyle::Kannada)); answer.append(ListStyleItem(i18n("Malayalam"), KoListStyle::Malayalam)); answer.append(ListStyleItem(i18n("Oriya"), KoListStyle::Oriya)); answer.append(ListStyleItem(i18n("Tamil"), KoListStyle::Tamil)); answer.append(ListStyleItem(i18n("Telugu"), KoListStyle::Telugu)); answer.append(ListStyleItem(i18n("Tibetan"), KoListStyle::Tibetan)); answer.append(ListStyleItem(i18n("Thai"), KoListStyle::Thai)); answer.append(ListStyleItem(i18n("Abjad"), KoListStyle::Abjad)); answer.append(ListStyleItem(i18n("AbjadMinor"), KoListStyle::AbjadMinor)); answer.append(ListStyleItem(i18n("ArabicAlphabet"), KoListStyle::ArabicAlphabet)); answer.append(ListStyleItem(i18n("Image"), KoListStyle::ImageItem)); return answer; } // ------------------- ListItemsHelper ------------ /// \internal helper class for calculating text-lists prefixes and indents ListItemsHelper::ListItemsHelper(QTextList *textList, const QFont &font) : m_textList(textList) , m_fm(font, textList->document()->documentLayout()->paintDevice()) { } void ListItemsHelper::recalculateBlock(QTextBlock &block) { //warnTextLayout; const QTextListFormat format = m_textList->format(); const KoListStyle::Style listStyle = static_cast(format.style()); const QString prefix = format.stringProperty(KoListStyle::ListItemPrefix); const QString suffix = format.stringProperty(KoListStyle::ListItemSuffix); const int level = format.intProperty(KoListStyle::Level); int dp = format.intProperty(KoListStyle::DisplayLevel); if (dp > level) dp = level; const int displayLevel = dp ? dp : 1; QTextBlockFormat blockFormat = block.blockFormat(); // Look if we have a block that is inside a header. We need to special case them cause header-lists are // different from any other kind of list and they do build up there own global list (table of content). bool isOutline = blockFormat.intProperty(KoParagraphStyle::OutlineLevel) > 0; int startValue = 1; if (format.hasProperty(KoListStyle::StartValue)) startValue = format.intProperty(KoListStyle::StartValue); int index = startValue; bool fixed = false; if (blockFormat.boolProperty(KoParagraphStyle::RestartListNumbering)) { index = format.intProperty(KoListStyle::StartValue); fixed = true; } const int paragIndex = blockFormat.intProperty(KoParagraphStyle::ListStartValue); if (paragIndex > 0) { index = paragIndex; fixed = true; } if (!fixed) { //if this is the first item then find if the list has to be continued from any other list KoList *listContinued = 0; if (m_textList->itemNumber(block) == 0 && KoTextDocument(m_textList->document()).list(m_textList) && (listContinued = KoTextDocument(m_textList->document()).list(m_textList)->listContinuedFrom())) { //find the previous list of the same level QTextList *previousTextList = listContinued->textLists().at(level - 1).data(); if (previousTextList) { QTextBlock textBlock = previousTextList->item(previousTextList->count() - 1); if (textBlock.isValid()) { index = KoTextBlockData(textBlock).counterIndex() + 1; //resume the previous list count } } } else if (m_textList->itemNumber(block) > 0) { QTextBlock textBlock = m_textList->item(m_textList->itemNumber(block) - 1); if (textBlock.isValid()) { index = KoTextBlockData(textBlock).counterIndex() + 1; //resume the previous list count } } } qreal width = 0.0; KoTextBlockData blockData(block); if (blockFormat.boolProperty(KoParagraphStyle::UnnumberedListItem) || blockFormat.boolProperty(KoParagraphStyle::IsListHeader)) { blockData.setCounterPlainText(QString()); blockData.setCounterPrefix(QString()); blockData.setCounterSuffix(QString()); blockData.setPartialCounterText(QString()); // set the counter for the current un-numbered list to the counter index of the previous list item. // index-1 because the list counter would have already incremented by one blockData.setCounterIndex(index - 1); if (blockFormat.boolProperty(KoParagraphStyle::IsListHeader)) { blockData.setCounterWidth(format.doubleProperty(KoListStyle::MinimumWidth)); blockData.setCounterSpacing(0); } return; } QString item; if (displayLevel > 1) { int checkLevel = level; int tmpDisplayLevel = displayLevel; bool counterResetRequired = true; for (QTextBlock b = block.previous(); tmpDisplayLevel > 1 && b.isValid(); b = b.previous()) { if (b.textList() == 0) continue; QTextListFormat lf = b.textList()->format(); if (lf.property(KoListStyle::StyleId) != format.property(KoListStyle::StyleId)) continue; // uninteresting for us if (isOutline != bool(b.blockFormat().intProperty(KoParagraphStyle::OutlineLevel))) continue; // also uninteresting cause the one is an outline-listitem while the other is not if (! KoListStyle::isNumberingStyle(static_cast(lf.style()))) { continue; } if (b.blockFormat().boolProperty(KoParagraphStyle::UnnumberedListItem)) { continue; //unnumbered listItems are irrelevant } const int otherLevel = lf.intProperty(KoListStyle::Level); if (isOutline && checkLevel == otherLevel) { counterResetRequired = false; } if (checkLevel <= otherLevel) continue; KoTextBlockData otherData(b); if (!otherData.hasCounterData()) { continue; } if (tmpDisplayLevel - 1 < otherLevel) { // can't just copy it fully since we are // displaying less then the full counter item += otherData.partialCounterText(); tmpDisplayLevel--; checkLevel--; for (int i = otherLevel + 1; i < level; i++) { tmpDisplayLevel--; item += "." + intToNumberingStyle(index, listStyle, m_textList->format().boolProperty(KoListStyle::LetterSynchronization)); // add missing counters. } } else { // just copy previous counter as prefix QString otherPrefix = lf.stringProperty(KoListStyle::ListItemPrefix); QString otherSuffix = lf.stringProperty(KoListStyle::ListItemSuffix); QString pureCounter = otherData.counterText().mid(otherPrefix.size()); pureCounter = pureCounter.left(pureCounter.size() - otherSuffix.size()); item += pureCounter; for (int i = otherLevel + 1; i < level; i++) item += "." + intToNumberingStyle(index, listStyle, m_textList->format().boolProperty(KoListStyle::LetterSynchronization)); // add missing counters. tmpDisplayLevel = 0; if (isOutline && counterResetRequired) { index = 1; } break; } } for (int i = 1; i < tmpDisplayLevel; i++) item = intToNumberingStyle(index, listStyle, m_textList->format().boolProperty(KoListStyle::LetterSynchronization)) + "." + item; // add missing counters. } if ((listStyle == KoListStyle::DecimalItem || listStyle == KoListStyle::AlphaLowerItem || listStyle == KoListStyle::UpperAlphaItem || listStyle == KoListStyle::RomanLowerItem || listStyle == KoListStyle::UpperRomanItem) && !(item.isEmpty() || item.endsWith('.') || item.endsWith(' '))) { item += '.'; } bool calcWidth = true; QString partialCounterText; if (KoListStyle::isNumberingStyle(listStyle)) { partialCounterText = intToNumberingStyle(index, listStyle, m_textList->format().boolProperty(KoListStyle::LetterSynchronization)); } else { switch (listStyle) { case KoListStyle::SquareItem: case KoListStyle::Bullet: case KoListStyle::BlackCircle: case KoListStyle::DiscItem: case KoListStyle::CircleItem: case KoListStyle::HeavyCheckMarkItem: case KoListStyle::BallotXItem: case KoListStyle::RightArrowItem: case KoListStyle::RightArrowHeadItem: case KoListStyle::RhombusItem: case KoListStyle::BoxItem: { calcWidth = false; if (format.intProperty(KoListStyle::BulletCharacter)) item = QString(QChar(format.intProperty(KoListStyle::BulletCharacter))); width = m_fm.width(item); int percent = format.intProperty(KoListStyle::RelativeBulletSize); if (percent > 0) width = width * (percent / 100.0); break; } case KoListStyle::CustomCharItem: calcWidth = false; if (format.intProperty(KoListStyle::BulletCharacter)) item = QString(QChar(format.intProperty(KoListStyle::BulletCharacter))); width = m_fm.width(item); break; case KoListStyle::None: calcWidth = false; width = 0.0; break; case KoListStyle::ImageItem: calcWidth = false; width = qMax(format.doubleProperty(KoListStyle::Width), (qreal)1.0); break; default: // others we ignore. calcWidth = false; } } blockData.setCounterIsImage(listStyle == KoListStyle::ImageItem); blockData.setPartialCounterText(partialCounterText); blockData.setCounterIndex(index); item += partialCounterText; blockData.setCounterPlainText(item); blockData.setCounterPrefix(prefix); blockData.setCounterSuffix(suffix); if (calcWidth) width = m_fm.width(item); index++; width += m_fm.width(prefix + suffix); qreal counterSpacing = 0; if (format.boolProperty(KoListStyle::AlignmentMode)) { - // for aligmentmode spacing should be 0 + // for AlignmentMode spacing should be 0 counterSpacing = 0; } else { if (listStyle != KoListStyle::None) { // see ODF spec 1.2 item 20.422 counterSpacing = format.doubleProperty(KoListStyle::MinimumDistance); if (width < format.doubleProperty(KoListStyle::MinimumWidth)) { counterSpacing -= format.doubleProperty(KoListStyle::MinimumWidth) - width; } counterSpacing = qMax(counterSpacing, qreal(0.0)); } width = qMax(width, format.doubleProperty(KoListStyle::MinimumWidth)); } blockData.setCounterWidth(width); blockData.setCounterSpacing(counterSpacing); //warnTextLayout; } // static bool ListItemsHelper::needsRecalc(QTextList *textList) { Q_ASSERT(textList); QTextBlock tb = textList->item(0); KoTextBlockData blockData(tb); return !blockData.hasCounterData(); } diff --git a/plugins/flake/textshape/textlayout/Mainpage.dox b/plugins/flake/textshape/textlayout/Mainpage.dox index fb5841e941..9563c9ce8b 100644 --- a/plugins/flake/textshape/textlayout/Mainpage.dox +++ b/plugins/flake/textshape/textlayout/Mainpage.dox @@ -1,65 +1,65 @@ /** * \mainpage * * KoText is a library for general use that extends the QText framework * (codenamed scribe) with an enhanced text-layout which adds features * required by ODF and general word-processing applications. * * You can use KoText at all places where you would normally use a * QTextDocument as the main text layout class in scribe can be replaced * on any QTextDocument instance using QTextDocument::setDocumentLayout(). * This means you can use the Qt API as normal, but you will be * able to use extra features like the plugins, the variables and the * ODF loading and saving for all the ODF text-layout features. * * TextShape Flake-Plugin * * Closely coupled with the kotext library is the text plugin that is * build on flake technology. All the user interaction dialogs and code * will reside in that plugin, and the actual heavy lifting of the * layout also is present only in that plugin. In other words; this * library will supply you with the APIs but without having the text * shape plugin loaded you can't show or layout the text. The goal is * to keep it cheap to link to this library and only provide the bare * minimum of functionality is the way to get there. The feature- * package will be completed by the optional text-plugin. * * QTextDocument compatibility * * The actual content is stored in the QTextDocument, as mentioned before. * In KoText we support a lot more features than Qt does in its layout * and this library will allow you to enrich your document with those * features. The core design goal is that you can use an externally * created QTextDocument with KoText. This has the implication that all * the extra content is stored inside the document. We add QTextFormat * based properties for that as can be seen in the styles (see - * KoParagraphStyle::Properities for instance), and we allow managers to + * KoParagraphStyle::Properties for instance), and we allow managers to * be stored on the document too. So for example a KoStyleManager will * be stored as a property on the QTextDocument and you can access that * using the KoTextDocument API. Note that you can use the * KoTextDocument class while using only a QTextDocument instance. * * Plugins * * There are various plugins for KoText that make it possible for 3rd * parties to extend the KoText functionality, see the techbase page; * http://techbase.kde.org/Development/Tutorials/Calligra_Overview#Text_Plugins * * ODF compatibility * * Loading and saving of documents can be done to and from ODF using the * open document classes. * * Important classes; * * KoTextDocumentLayout the main layout class to be set on the QTextDocument. * * KoInlineTextObject plugin base for inline objects (and variables) * * KoTextEditingPlugin plugin base for text editing plugins. * * KoText namespace. */ // DOXYGEN_SET_PROJECT_NAME = KritaTextLayout // DOXYGEN_SET_IGNORE_PREFIX = Ko K diff --git a/plugins/impex/jpeg/kis_jpeg_converter.cc b/plugins/impex/jpeg/kis_jpeg_converter.cc index 976ec57a3c..0a759730d4 100644 --- a/plugins/impex/jpeg/kis_jpeg_converter.cc +++ b/plugins/impex/jpeg/kis_jpeg_converter.cc @@ -1,732 +1,732 @@ /* * Copyright (c) 2005 Cyrille Berger * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kis_jpeg_converter.h" #include #include #include #ifdef HAVE_LCMS2 # include #else # include #endif extern "C" { #include } #include #include #include #include #include #include #include #include #include #include #include #include #include "KoColorModelStandardIds.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_iterator_ng.h" #define ICC_MARKER (JPEG_APP0 + 2) /* JPEG marker code for ICC */ #define ICC_OVERHEAD_LEN 14 /* size of non-profile data in APP2 */ #define MAX_BYTES_IN_MARKER 65533 /* maximum data len of a JPEG marker */ #define MAX_DATA_BYTES_IN_MARKER (MAX_BYTES_IN_MARKER - ICC_OVERHEAD_LEN) const char photoshopMarker[] = "Photoshop 3.0\0"; //const char photoshopBimId_[] = "8BIM"; const uint16_t photoshopIptc = 0x0404; const char xmpMarker[] = "http://ns.adobe.com/xap/1.0/\0"; const QByteArray photoshopIptc_((char*)&photoshopIptc, 2); namespace { void jpegErrorExit ( j_common_ptr cinfo ) { char jpegLastErrorMsg[JMSG_LENGTH_MAX]; /* Create the message */ ( *( cinfo->err->format_message ) ) ( cinfo, jpegLastErrorMsg ); /* Jump to the setjmp point */ - throw std::runtime_error( jpegLastErrorMsg ); // or your preffered exception ... + throw std::runtime_error( jpegLastErrorMsg ); // or your preferred exception ... } J_COLOR_SPACE getColorTypeforColorSpace(const KoColorSpace * cs) { if (KoID(cs->id()) == KoID("GRAYA") || cs->id() == "GRAYAU16" || cs->id() == "GRAYA16") { return JCS_GRAYSCALE; } if (KoID(cs->id()) == KoID("RGBA") || KoID(cs->id()) == KoID("RGBA16")) { return JCS_RGB; } if (KoID(cs->id()) == KoID("CMYK") || KoID(cs->id()) == KoID("CMYKAU16")) { return JCS_CMYK; } return JCS_UNKNOWN; } QString getColorSpaceModelForColorType(J_COLOR_SPACE color_type) { dbgFile << "color_type =" << color_type; if (color_type == JCS_GRAYSCALE) { return GrayAColorModelID.id(); } else if (color_type == JCS_RGB) { return RGBAColorModelID.id(); } else if (color_type == JCS_CMYK) { return CMYKAColorModelID.id(); } return ""; } } struct KisJPEGConverter::Private { Private(KisDocument *doc, bool batchMode) : doc(doc), stop(false), batchMode(batchMode) {} KisImageSP image; KisDocument *doc; bool stop; bool batchMode; }; KisJPEGConverter::KisJPEGConverter(KisDocument *doc, bool batchMode) : m_d(new Private(doc, batchMode)) { } KisJPEGConverter::~KisJPEGConverter() { } KisImageBuilder_Result KisJPEGConverter::decode(QIODevice *io) { struct jpeg_decompress_struct cinfo; struct jpeg_error_mgr jerr; cinfo.err = jpeg_std_error(&jerr); jerr.error_exit = jpegErrorExit; try { jpeg_create_decompress(&cinfo); KisJPEGSource::setSource(&cinfo, io); jpeg_save_markers(&cinfo, JPEG_COM, 0xFFFF); /* Save APP0..APP15 markers */ for (int m = 0; m < 16; m++) jpeg_save_markers(&cinfo, JPEG_APP0 + m, 0xFFFF); // setup_read_icc_profile(&cinfo); // read header jpeg_read_header(&cinfo, (boolean)true); // start reading jpeg_start_decompress(&cinfo); // Get the colorspace QString modelId = getColorSpaceModelForColorType(cinfo.out_color_space); if (modelId.isEmpty()) { dbgFile << "unsupported colorspace :" << cinfo.out_color_space; jpeg_destroy_decompress(&cinfo); return KisImageBuilder_RESULT_UNSUPPORTED_COLORSPACE; } uchar* profile_data; uint profile_len; const KoColorProfile* profile = 0; QByteArray profile_rawdata; if (read_icc_profile(&cinfo, &profile_data, &profile_len)) { profile_rawdata.resize(profile_len); memcpy(profile_rawdata.data(), profile_data, profile_len); cmsHPROFILE hProfile = cmsOpenProfileFromMem(profile_data, profile_len); if (hProfile != (cmsHPROFILE) 0) { profile = KoColorSpaceRegistry::instance()->createColorProfile(modelId, Integer8BitsColorDepthID.id(), profile_rawdata); Q_CHECK_PTR(profile); dbgFile <<"profile name:" << profile->name() <<" product information:" << profile->info(); if (!profile->isSuitableForOutput()) { dbgFile << "the profile is not suitable for output and therefore cannot be used in krita, we need to convert the image to a standard profile"; // TODO: in ko2 popup a selection menu to inform the user } } } const QString colorSpaceId = KoColorSpaceRegistry::instance()->colorSpaceId(modelId, Integer8BitsColorDepthID.id()); // Check that the profile is used by the color space if (profile && !KoColorSpaceRegistry::instance()->profileIsCompatible(profile, colorSpaceId)) { warnFile << "The profile " << profile->name() << " is not compatible with the color space model " << modelId; profile = 0; } // Retrieve a pointer to the colorspace const KoColorSpace* cs; if (profile && profile->isSuitableForOutput()) { dbgFile << "image has embedded profile:" << profile -> name() << ""; cs = KoColorSpaceRegistry::instance()->colorSpace(modelId, Integer8BitsColorDepthID.id(), profile); } else cs = KoColorSpaceRegistry::instance()->colorSpace(modelId, Integer8BitsColorDepthID.id(), ""); if (cs == 0) { dbgFile << "unknown colorspace"; jpeg_destroy_decompress(&cinfo); return KisImageBuilder_RESULT_UNSUPPORTED_COLORSPACE; } // TODO fixit // Create the cmsTransform if needed KoColorTransformation* transform = 0; if (profile && !profile->isSuitableForOutput()) { transform = KoColorSpaceRegistry::instance()->colorSpace(modelId, Integer8BitsColorDepthID.id(), profile)->createColorConverter(cs, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); } // Apparently an invalid transform was created from the profile. See bug https://bugs.kde.org/show_bug.cgi?id=255451. // After 2.3: warn the user! if (transform && !transform->isValid()) { delete transform; transform = 0; } // Creating the KisImageSP if (!m_d->image) { m_d->image = new KisImage(m_d->doc->createUndoStore(), cinfo.image_width, cinfo.image_height, cs, "built image"); Q_CHECK_PTR(m_d->image); } // Set resolution double xres = 72, yres = 72; if (cinfo.density_unit == 1) { xres = cinfo.X_density; yres = cinfo.Y_density; } else if (cinfo.density_unit == 2) { xres = cinfo.X_density * 2.54; yres = cinfo.Y_density * 2.54; } if (xres < 72) { xres = 72; } if (yres < 72) { yres = 72; } m_d->image->setResolution(POINT_TO_INCH(xres), POINT_TO_INCH(yres)); // It is the "invert" macro because we convert from pointer-per-inchs to points // Create layer KisPaintLayerSP layer = KisPaintLayerSP(new KisPaintLayer(m_d->image.data(), m_d->image -> nextLayerName(), quint8_MAX)); // Read data JSAMPROW row_pointer = new JSAMPLE[cinfo.image_width*cinfo.num_components]; for (; cinfo.output_scanline < cinfo.image_height;) { KisHLineIteratorSP it = layer->paintDevice()->createHLineIteratorNG(0, cinfo.output_scanline, cinfo.image_width); jpeg_read_scanlines(&cinfo, &row_pointer, 1); quint8 *src = row_pointer; switch (cinfo.out_color_space) { case JCS_GRAYSCALE: do { quint8 *d = it->rawData(); d[0] = *(src++); if (transform) transform->transform(d, d, 1); d[1] = quint8_MAX; } while (it->nextPixel()); break; case JCS_RGB: do { quint8 *d = it->rawData(); d[2] = *(src++); d[1] = *(src++); d[0] = *(src++); if (transform) transform->transform(d, d, 1); d[3] = quint8_MAX; } while (it->nextPixel()); break; case JCS_CMYK: do { quint8 *d = it->rawData(); d[0] = quint8_MAX - *(src++); d[1] = quint8_MAX - *(src++); d[2] = quint8_MAX - *(src++); d[3] = quint8_MAX - *(src++); if (transform) transform->transform(d, d, 1); d[4] = quint8_MAX; } while (it->nextPixel()); break; default: return KisImageBuilder_RESULT_UNSUPPORTED; } } m_d->image->addNode(KisNodeSP(layer.data()), m_d->image->rootLayer().data()); // Read exif information dbgFile << "Looking for exif information"; for (jpeg_saved_marker_ptr marker = cinfo.marker_list; marker != 0; marker = marker->next) { dbgFile << "Marker is" << marker->marker; if (marker->marker != (JOCTET)(JPEG_APP0 + 1) || marker->data_length < 14) { continue; /* Exif data is in an APP1 marker of at least 14 octets */ } if (GETJOCTET(marker->data[0]) != (JOCTET) 0x45 || GETJOCTET(marker->data[1]) != (JOCTET) 0x78 || GETJOCTET(marker->data[2]) != (JOCTET) 0x69 || GETJOCTET(marker->data[3]) != (JOCTET) 0x66 || GETJOCTET(marker->data[4]) != (JOCTET) 0x00 || GETJOCTET(marker->data[5]) != (JOCTET) 0x00) continue; /* no Exif header */ dbgFile << "Found exif information of length :" << marker->data_length; KisMetaData::IOBackend* exifIO = KisMetaData::IOBackendRegistry::instance()->value("exif"); Q_ASSERT(exifIO); QByteArray byteArray((const char*)marker->data + 6, marker->data_length - 6); QBuffer buf(&byteArray); exifIO->loadFrom(layer->metaData(), &buf); // Interpret orientation tag if (layer->metaData()->containsEntry("http://ns.adobe.com/tiff/1.0/", "Orientation")) { KisMetaData::Entry& entry = layer->metaData()->getEntry("http://ns.adobe.com/tiff/1.0/", "Orientation"); if (entry.value().type() == KisMetaData::Value::Variant) { switch (entry.value().asVariant().toInt()) { case 2: KisTransformWorker::mirrorY(layer->paintDevice()); break; case 3: image()->rotateImage(M_PI); break; case 4: KisTransformWorker::mirrorX(layer->paintDevice()); break; case 5: image()->rotateImage(M_PI / 2); KisTransformWorker::mirrorY(layer->paintDevice()); break; case 6: image()->rotateImage(M_PI / 2); break; case 7: image()->rotateImage(M_PI / 2); KisTransformWorker::mirrorX(layer->paintDevice()); break; case 8: image()->rotateImage(-M_PI / 2 + M_PI*2); break; default: break; } } entry.value().setVariant(1); } break; } dbgFile << "Looking for IPTC information"; for (jpeg_saved_marker_ptr marker = cinfo.marker_list; marker != 0; marker = marker->next) { dbgFile << "Marker is" << marker->marker; if (marker->marker != (JOCTET)(JPEG_APP0 + 13) || marker->data_length < 14) { continue; /* IPTC data is in an APP13 marker of at least 16 octets */ } if (memcmp(marker->data, photoshopMarker, 14) != 0) { for (int i = 0; i < 14; i++) { dbgFile << (int)(*(marker->data + i)) << "" << (int)(photoshopMarker[i]); } dbgFile << "No photoshop marker"; continue; /* No IPTC Header */ } dbgFile << "Found Photoshop information of length :" << marker->data_length; KisMetaData::IOBackend* iptcIO = KisMetaData::IOBackendRegistry::instance()->value("iptc"); Q_ASSERT(iptcIO); const Exiv2::byte *record = 0; uint32_t sizeIptc = 0; uint32_t sizeHdr = 0; // Find actual Iptc data within the APP13 segment if (!Exiv2::Photoshop::locateIptcIrb((Exiv2::byte*)(marker->data + 14), marker->data_length - 14, &record, &sizeHdr, &sizeIptc)) { if (sizeIptc) { // Decode the IPTC data QByteArray byteArray((const char*)(record + sizeHdr), sizeIptc); QBuffer buf(&byteArray); iptcIO->loadFrom(layer->metaData(), &buf); } else { dbgFile << "IPTC Not found in Photoshop marker"; } } break; } dbgFile << "Looking for XMP information"; for (jpeg_saved_marker_ptr marker = cinfo.marker_list; marker != 0; marker = marker->next) { dbgFile << "Marker is" << marker->marker; if (marker->marker != (JOCTET)(JPEG_APP0 + 1) || marker->data_length < 31) { continue; /* XMP data is in an APP1 marker of at least 31 octets */ } if (memcmp(marker->data, xmpMarker, 29) != 0) { dbgFile << "Not XMP marker"; continue; /* No xmp Header */ } dbgFile << "Found XMP Marker of length " << marker->data_length; QByteArray byteArray((const char*)marker->data + 29, marker->data_length - 29); KisMetaData::IOBackend* xmpIO = KisMetaData::IOBackendRegistry::instance()->value("xmp"); Q_ASSERT(xmpIO); xmpIO->loadFrom(layer->metaData(), new QBuffer(&byteArray)); break; } // Dump loaded metadata layer->metaData()->debugDump(); // Check whether the metadata has resolution info, too... if (cinfo.density_unit == 0 && layer->metaData()->containsEntry("tiff:XResolution") && layer->metaData()->containsEntry("tiff:YResolution")) { double xres = layer->metaData()->getEntry("tiff:XResolution").value().asDouble(); double yres = layer->metaData()->getEntry("tiff:YResolution").value().asDouble(); if (xres != 0 && yres != 0) { m_d->image->setResolution(POINT_TO_INCH(xres), POINT_TO_INCH(yres)); // It is the "invert" macro because we convert from pointer-per-inchs to points } } // Finish decompression jpeg_finish_decompress(&cinfo); jpeg_destroy_decompress(&cinfo); delete [] row_pointer; return KisImageBuilder_RESULT_OK; } catch( std::runtime_error &e) { jpeg_destroy_decompress(&cinfo); return KisImageBuilder_RESULT_FAILURE; } } KisImageBuilder_Result KisJPEGConverter::buildImage(QIODevice *io) { return decode(io); } KisImageSP KisJPEGConverter::image() { return m_d->image; } KisImageBuilder_Result KisJPEGConverter::buildFile(QIODevice *io, KisPaintLayerSP layer, KisJPEGOptions options, KisMetaData::Store* metaData) { if (!layer) return KisImageBuilder_RESULT_INVALID_ARG; KisImageSP image = KisImageSP(layer->image()); if (!image) return KisImageBuilder_RESULT_EMPTY; const KoColorSpace * cs = layer->colorSpace(); J_COLOR_SPACE color_type = getColorTypeforColorSpace(cs); if (color_type == JCS_UNKNOWN) { KUndo2Command *tmp = layer->paintDevice()->convertTo(KoColorSpaceRegistry::instance()->rgb8(), KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); delete tmp; cs = KoColorSpaceRegistry::instance()->rgb8(); color_type = JCS_RGB; } if (options.forceSRGB) { const KoColorSpace* dst = KoColorSpaceRegistry::instance()->colorSpace(RGBAColorModelID.id(), layer->colorSpace()->colorDepthId().id(), "sRGB built-in - (lcms internal)"); KUndo2Command *tmp = layer->paintDevice()->convertTo(dst); delete tmp; cs = dst; color_type = JCS_RGB; } uint height = image->height(); uint width = image->width(); // Initialize structure struct jpeg_compress_struct cinfo; // Initialize error output struct jpeg_error_mgr jerr; cinfo.err = jpeg_std_error(&jerr); jpeg_create_compress(&cinfo); // Initialize output stream KisJPEGDestination::setDestination(&cinfo, io); cinfo.image_width = width; // image width and height, in pixels cinfo.image_height = height; cinfo.input_components = cs->colorChannelCount(); // number of color channels per pixel */ cinfo.in_color_space = color_type; // colorspace of input image // Set default compression parameters jpeg_set_defaults(&cinfo); // Customize them jpeg_set_quality(&cinfo, options.quality, (boolean)options.baseLineJPEG); if (options.progressive) { jpeg_simple_progression(&cinfo); } // Optimize ? cinfo.optimize_coding = (boolean)options.optimize; // Smoothing cinfo.smoothing_factor = (boolean)options.smooth; // Subsampling switch (options.subsampling) { default: case 0: { cinfo.comp_info[0].h_samp_factor = 2; cinfo.comp_info[0].v_samp_factor = 2; cinfo.comp_info[1].h_samp_factor = 1; cinfo.comp_info[1].v_samp_factor = 1; cinfo.comp_info[2].h_samp_factor = 1; cinfo.comp_info[2].v_samp_factor = 1; } break; case 1: { cinfo.comp_info[0].h_samp_factor = 2; cinfo.comp_info[0].v_samp_factor = 1; cinfo.comp_info[1].h_samp_factor = 1; cinfo.comp_info[1].v_samp_factor = 1; cinfo.comp_info[2].h_samp_factor = 1; cinfo.comp_info[2].v_samp_factor = 1; } break; case 2: { cinfo.comp_info[0].h_samp_factor = 1; cinfo.comp_info[0].v_samp_factor = 2; cinfo.comp_info[1].h_samp_factor = 1; cinfo.comp_info[1].v_samp_factor = 1; cinfo.comp_info[2].h_samp_factor = 1; cinfo.comp_info[2].v_samp_factor = 1; } break; case 3: { cinfo.comp_info[0].h_samp_factor = 1; cinfo.comp_info[0].v_samp_factor = 1; cinfo.comp_info[1].h_samp_factor = 1; cinfo.comp_info[1].v_samp_factor = 1; cinfo.comp_info[2].h_samp_factor = 1; cinfo.comp_info[2].v_samp_factor = 1; } break; } // Save resolution cinfo.X_density = INCH_TO_POINT(image->xRes()); // It is the "invert" macro because we convert from pointer-per-inchs to points cinfo.Y_density = INCH_TO_POINT(image->yRes()); // It is the "invert" macro because we convert from pointer-per-inchs to points cinfo.density_unit = 1; cinfo.write_JFIF_header = (boolean)true; // Start compression jpeg_start_compress(&cinfo, (boolean)true); // Save exif and iptc information if any available if (metaData && !metaData->empty()) { metaData->applyFilters(options.filters); // Save EXIF if (options.exif) { dbgFile << "Trying to save exif information"; KisMetaData::IOBackend* exifIO = KisMetaData::IOBackendRegistry::instance()->value("exif"); Q_ASSERT(exifIO); QBuffer buffer; exifIO->saveTo(metaData, &buffer, KisMetaData::IOBackend::JpegHeader); dbgFile << "Exif information size is" << buffer.data().size(); QByteArray data = buffer.data(); if (data.size() < MAX_DATA_BYTES_IN_MARKER) { jpeg_write_marker(&cinfo, JPEG_APP0 + 1, (const JOCTET*)data.data(), data.size()); } else { dbgFile << "EXIF information could not be saved."; // TODO: warn the user ? } } // Save IPTC if (options.iptc) { dbgFile << "Trying to save exif information"; KisMetaData::IOBackend* iptcIO = KisMetaData::IOBackendRegistry::instance()->value("iptc"); Q_ASSERT(iptcIO); QBuffer buffer; iptcIO->saveTo(metaData, &buffer, KisMetaData::IOBackend::JpegHeader); dbgFile << "IPTC information size is" << buffer.data().size(); QByteArray data = buffer.data(); if (data.size() < MAX_DATA_BYTES_IN_MARKER) { jpeg_write_marker(&cinfo, JPEG_APP0 + 13, (const JOCTET*)data.data(), data.size()); } else { dbgFile << "IPTC information could not be saved."; // TODO: warn the user ? } } // Save XMP if (options.xmp) { dbgFile << "Trying to save XMP information"; KisMetaData::IOBackend* xmpIO = KisMetaData::IOBackendRegistry::instance()->value("xmp"); Q_ASSERT(xmpIO); QBuffer buffer; xmpIO->saveTo(metaData, &buffer, KisMetaData::IOBackend::JpegHeader); dbgFile << "XMP information size is" << buffer.data().size(); QByteArray data = buffer.data(); if (data.size() < MAX_DATA_BYTES_IN_MARKER) { jpeg_write_marker(&cinfo, JPEG_APP0 + 14, (const JOCTET*)data.data(), data.size()); } else { dbgFile << "XMP information could not be saved."; // TODO: warn the user ? } } } KisPaintDeviceSP dev = new KisPaintDevice(layer->colorSpace()); KoColor c(options.transparencyFillColor, layer->colorSpace()); dev->fill(QRect(0, 0, width, height), c); KisPainter gc(dev); gc.bitBlt(QPoint(0, 0), layer->paintDevice(), QRect(0, 0, width, height)); gc.end(); if (options.saveProfile) { const KoColorProfile* colorProfile = layer->colorSpace()->profile(); QByteArray colorProfileData = colorProfile->rawData(); write_icc_profile(& cinfo, (uchar*) colorProfileData.data(), colorProfileData.size()); } // Write data information JSAMPROW row_pointer = new JSAMPLE[width*cinfo.input_components]; int color_nb_bits = 8 * layer->paintDevice()->pixelSize() / layer->paintDevice()->channelCount(); for (; cinfo.next_scanline < height;) { KisHLineConstIteratorSP it = dev->createHLineConstIteratorNG(0, cinfo.next_scanline, width); quint8 *dst = row_pointer; switch (color_type) { case JCS_GRAYSCALE: if (color_nb_bits == 16) { do { //const quint16 *d = reinterpret_cast(it->oldRawData()); const quint8 *d = it->oldRawData(); *(dst++) = cs->scaleToU8(d, 0);//d[0] / quint8_MAX; } while (it->nextPixel()); } else { do { const quint8 *d = it->oldRawData(); *(dst++) = d[0]; } while (it->nextPixel()); } break; case JCS_RGB: if (color_nb_bits == 16) { do { //const quint16 *d = reinterpret_cast(it->oldRawData()); const quint8 *d = it->oldRawData(); *(dst++) = cs->scaleToU8(d, 2); //d[2] / quint8_MAX; *(dst++) = cs->scaleToU8(d, 1); //d[1] / quint8_MAX; *(dst++) = cs->scaleToU8(d, 0); //d[0] / quint8_MAX; } while (it->nextPixel()); } else { do { const quint8 *d = it->oldRawData(); *(dst++) = d[2]; *(dst++) = d[1]; *(dst++) = d[0]; } while (it->nextPixel()); } break; case JCS_CMYK: if (color_nb_bits == 16) { do { //const quint16 *d = reinterpret_cast(it->oldRawData()); const quint8 *d = it->oldRawData(); *(dst++) = quint8_MAX - cs->scaleToU8(d, 0);//quint8_MAX - d[0] / quint8_MAX; *(dst++) = quint8_MAX - cs->scaleToU8(d, 1);//quint8_MAX - d[1] / quint8_MAX; *(dst++) = quint8_MAX - cs->scaleToU8(d, 2);//quint8_MAX - d[2] / quint8_MAX; *(dst++) = quint8_MAX - cs->scaleToU8(d, 3);//quint8_MAX - d[3] / quint8_MAX; } while (it->nextPixel()); } else { do { const quint8 *d = it->oldRawData(); *(dst++) = quint8_MAX - d[0]; *(dst++) = quint8_MAX - d[1]; *(dst++) = quint8_MAX - d[2]; *(dst++) = quint8_MAX - d[3]; } while (it->nextPixel()); } break; default: delete [] row_pointer; jpeg_destroy_compress(&cinfo); return KisImageBuilder_RESULT_UNSUPPORTED; } jpeg_write_scanlines(&cinfo, &row_pointer, 1); } // Writing is over jpeg_finish_compress(&cinfo); delete [] row_pointer; // Free memory jpeg_destroy_compress(&cinfo); return KisImageBuilder_RESULT_OK; } void KisJPEGConverter::cancel() { m_d->stop = true; } diff --git a/plugins/impex/xcf/3rdparty/xcftools/xcf-general.c b/plugins/impex/xcf/3rdparty/xcftools/xcf-general.c index 371691782f..e5ae5a6049 100644 --- a/plugins/impex/xcf/3rdparty/xcftools/xcf-general.c +++ b/plugins/impex/xcf/3rdparty/xcftools/xcf-general.c @@ -1,305 +1,305 @@ /* Generic functions for reading XCF files * * This file was written by Henning Makholm * It is hereby in the public domain. * * In jurisdictions that do not recognise grants of copyright to the * public domain: I, the author and (presumably, in those jurisdictions) * copyright holder, hereby permit anyone to distribute and use this code, * in source code or binary form, with or without modifications. This * permission is world-wide and irrevocable. * * Of course, I will not be liable for any errors or shortcomings in the * code, since I give it away without asking any compenstations. * * If you use or distribute this code, I would appreciate receiving * credit for writing it, in whichever way you find proper and customary. */ #include "xcftools.h" #include #include #ifdef HAVE_ICONV # include #elif !defined(ICONV_CONST) # define ICONV_CONST const #endif uint8_t *xcf_file = 0 ; size_t xcf_length ; int use_utf8 = 0 ; uint32_t xcfOffset(uint32_t addr,int spaceafter) { uint32_t apparent ; xcfCheckspace(addr,4,"(xcfOffset)"); apparent = xcfL(addr); xcfCheckspace(apparent,spaceafter, "Too large offset (%" PRIX32 ") at position %" PRIX32, apparent,addr); return apparent ; } int xcfNextprop(uint32_t *master,uint32_t *body) { uint32_t ptr, length, total, minlength ; PropType type ; ptr = *master ; xcfCheckspace(ptr,8,"(property header)"); type = xcfL(ptr); length = xcfL(ptr+4); *body = ptr+8 ; switch(type) { case PROP_COLORMAP: { uint32_t ncolors ; xcfCheckspace(ptr+8,4,"(colormap length)"); ncolors = xcfL(ptr+8) ; if( ncolors > 256 ) FatalBadXCF("Colormap has %" PRIu32 " entries",ncolors); - /* Surprise! Some older verion of the Gimp computed the wrong length + /* Surprise! Some older version of the Gimp computed the wrong length * word, and the _reader_ always just reads three bytes per color * and ignores the length tag! Duplicate this so we too can read * the buggy XCF files. */ length = minlength = 4+3*ncolors; break; } case PROP_COMPRESSION: minlength = 1; break; case PROP_OPACITY: minlength = 4; break; case PROP_APPLY_MASK: minlength = 4; break; case PROP_OFFSETS: minlength = 8; break; case PROP_MODE: minlength = 4; break; default: minlength = 0; break; } if( length < minlength ) FatalBadXCF("Short %s property at %" PRIX32 " (%" PRIu32 "<%" PRIu32 ")", showPropType(type),ptr,length,minlength); *master = ptr+8+length ; total = 8 + length + (type != PROP_END ? 8 : 0) ; if( total < length ) /* Check overwrap */ FatalBadXCF("Overlong property at %" PRIX32, ptr); xcfCheckspace(ptr,total,"Overlong property at %" PRIX32,ptr) ; return type ; } const char* xcfString(uint32_t ptr,uint32_t *after) { uint32_t length ; unsigned i ; ICONV_CONST char *utf8master ; xcfCheckspace(ptr,4,"(string length)"); length = xcfL(ptr) ; ptr += 4 ; xcfCheckspace(ptr,length,"(string)"); utf8master = (ICONV_CONST char*)(xcf_file+ptr) ; if( after ) *after = ptr + length ; if( length == 0 || utf8master[length-1] != 0 ) FatalBadXCF("String at %" PRIX32 " not zero-terminated",ptr-4); length-- ; if( use_utf8 ) return utf8master ; /* We assume that the local character set includes ASCII... * Check if conversion is needed at all */ for( i=0 ; ; i++ ) { if( i == length ) return utf8master ; /* Only ASCII after all */ if( utf8master[i] == 0 ) FatalBadXCF("String at %" PRIX32 " has embedded zeroes",ptr-4); if( (int8_t) utf8master[i] < 0 ) break ; } #ifdef HAVE_ICONV { size_t targetsize = length+1 ; int sloppy_translation = 0 ; iconv_t cd = iconv_open("//TRANSLIT","UTF-8"); if( cd == (iconv_t) -1 ) { cd = iconv_open("","UTF-8"); sloppy_translation = 1 ; } if( cd == (iconv_t) -1 ) iconv_close(cd) ; /* Give up; perhaps iconv doesn't know UTF-8 */ else while(1) { char *buffer = xcfmalloc(targetsize) ; ICONV_CONST char *inbuf = utf8master ; char *outbuf = buffer ; size_t incount = length ; size_t outcount = targetsize ; while(1) { /* Loop for systems without //ICONV support */ size_t result = iconv(cd,&inbuf,&incount,&outbuf,&outcount) ; if( result == (size_t)-1 && errno == EILSEQ && sloppy_translation && outcount > 0 ) { *outbuf++ = '?' ; outcount-- ; while( (int8_t)*inbuf < 0 ) inbuf++, incount-- ; continue ; } if( result != (size_t)-1 ) { if( outcount == 0 ) errno = E2BIG ; else { *outbuf = 0 ; iconv_close(cd) ; return buffer ; } } break ; } if( errno == EILSEQ || errno == EINVAL ) FatalBadXCF("Bad UTF-8 encoding '%s' at %" PRIXPTR, inbuf,(uintptr_t)((inbuf-utf8master)+ptr)); if( errno == E2BIG ) { targetsize += 1+incount ; xcffree(buffer) ; continue ; } FatalUnexpected("!iconv on layer name at %"PRIX32,ptr); } } #endif { static int warned = 0 ; if( !warned ) { fprintf(stderr,_("Warning: one or more layer names could not be\n" " translated to the local character set.\n")); warned = 1 ; } } return utf8master ; } /* ****************************************************************** */ void computeDimensions(struct tileDimensions *d) { d->c.r = d->c.l + d->width ; d->c.b = d->c.t + d->height ; d->tilesx = (d->width+TILE_WIDTH-1)/TILE_WIDTH ; d->tilesy = (d->height+TILE_HEIGHT-1)/TILE_HEIGHT ; d->ntiles = d->tilesx * d->tilesy ; } struct xcfImage XCF ; void getBasicXcfInfo(void) { uint32_t ptr, data, layerfile ; PropType type ; int i, j ; xcfCheckspace(0,14+7*4,"(very short)"); if( strcmp((char*)xcf_file,"gimp xcf file") == 0 ) XCF.version = 0 ; else if( xcf_file[13] == 0 && sscanf((char*)xcf_file,"gimp xcf v%d",&XCF.version) == 1 ) ; else FatalBadXCF(_("Not an XCF file at all (magic not recognized)")); if (XCF.version < 0 || XCF.version > 3) { return; } XCF.compression = COMPRESS_NONE ; XCF.colormapptr = 0 ; ptr = 14 ; XCF.width = xcfL(ptr); ptr += 4 ; XCF.height = xcfL(ptr); ptr += 4 ; XCF.type = xcfL(ptr); ptr += 4 ; while( (type = xcfNextprop(&ptr,&data)) != PROP_END ) { switch(type) { case PROP_COLORMAP: XCF.colormapptr = data ; break ; case PROP_COMPRESSION: XCF.compression = xcf_file[data] ; break ; default: /* Ignore unknown properties */ break ; } } layerfile = ptr ; for( XCF.numLayers = 0 ; xcfOffset(ptr,8*4) ; XCF.numLayers++, ptr+=4 ) ; XCF.layers = xcfmalloc(XCF.numLayers * sizeof(struct xcfLayer)) ; for( i = 0 ; i < XCF.numLayers ; i++ ) { struct xcfLayer *L = XCF.layers + i ; ptr = xcfL(layerfile+4*(XCF.numLayers-1-i)) ; L->mode = GIMP_NORMAL_MODE ; L->opacity = 255 ; L->isVisible = 1 ; L->hasMask = 0 ; L->dim.width = xcfL(ptr); ptr+=4 ; L->dim.height = xcfL(ptr); ptr+=4 ; L->type = xcfL(ptr); ptr+=4 ; L->name = xcfString(ptr,&ptr); L->propptr = ptr ; L->isGroup = 0; L->pathLength = 0; L->path = NULL; while( (type = xcfNextprop(&ptr,&data)) != PROP_END ) { switch(type) { case PROP_OPACITY: L->opacity = xcfL(data); if( L->opacity > 255 ) L->opacity = 255 ; break ; case PROP_VISIBLE: L->isVisible = xcfL(data) != 0 ; break ; case PROP_APPLY_MASK: L->hasMask = xcfL(data) != 0 ; break ; case PROP_OFFSETS: L->dim.c.l = (int32_t)(xcfL(data )) ; L->dim.c.t = (int32_t)(xcfL(data+4)) ; break ; case PROP_MODE: L->mode = xcfL(data); break ; case PROP_GROUP_ITEM: L->isGroup = 1 ; break; case PROP_ITEM_PATH: L->pathLength = (ptr - data - 2) / 4 ; if ( L->pathLength != 0 ) { L->path = xcfmalloc( L->pathLength * sizeof(unsigned) ) ; for ( j = 0; j!=L->pathLength; j++ ) *(L->path + j) = (unsigned)xcfL(data + 4 * j); } break; default: /* Ignore unknown properties */ break ; } } xcfCheckspace(ptr,8,"(end of layer %s)",L->name); L->pixels.tileptrs = 0 ; L->pixels.hierarchy = xcfOffset(ptr ,4*4); L->mask.tileptrs = 0 ; L->mask.hierarchy = xcfOffset(ptr+4,4*4); computeDimensions(&L->dim); } } diff --git a/plugins/python/comics_project_management_tools/comics_project_manager_docker.py b/plugins/python/comics_project_management_tools/comics_project_manager_docker.py index 38f618d801..f3640ac961 100644 --- a/plugins/python/comics_project_management_tools/comics_project_manager_docker.py +++ b/plugins/python/comics_project_management_tools/comics_project_manager_docker.py @@ -1,922 +1,924 @@ """ Copyright (c) 2017 Wolthera van Hövell tot Westerflier This file is part of the Comics Project Management Tools(CPMT). CPMT 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. CPMT 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 the CPMT. If not, see . """ """ This is a docker that helps you organise your comics project. """ import sys import json import os import zipfile # quick reading of documents import shutil import enum from math import floor import xml.etree.ElementTree as ET from PyQt5.QtCore import QElapsedTimer, QSize, Qt, QRect from PyQt5.QtGui import QStandardItem, QStandardItemModel, QImage, QIcon, QPixmap, QFontMetrics, QPainter, QPalette, QFont from PyQt5.QtWidgets import QHBoxLayout, QVBoxLayout, QListView, QToolButton, QMenu, QAction, QPushButton, QSpacerItem, QSizePolicy, QWidget, QAbstractItemView, QProgressDialog, QDialog, QFileDialog, QDialogButtonBox, qApp, QSplitter, QSlider, QLabel, QStyledItemDelegate, QStyle, QMessageBox import math from krita import * from . import comics_metadata_dialog, comics_exporter, comics_export_dialog, comics_project_setup_wizard, comics_template_dialog, comics_project_settings_dialog, comics_project_page_viewer, comics_project_translation_scraper """ A very simple class so we can have a label that is single line, but doesn't force the widget size to be bigger. This is used by the project name. """ class Elided_Text_Label(QLabel): mainText = str() def __init__(self, parent=None): super(QLabel, self).__init__(parent) self.setMinimumWidth(self.fontMetrics().width("...")) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) def setMainText(self, text=str()): self.mainText = text self.elideText() def elideText(self): self.setText(self.fontMetrics().elidedText(self.mainText, Qt.ElideRight, self.width())) def resizeEvent(self, event): self.elideText() class CPE(enum.IntEnum): TITLE = Qt.DisplayRole URL = Qt.UserRole + 1 KEYWORDS = Qt.UserRole+2 DESCRIPTION = Qt.UserRole+3 LASTEDIT = Qt.UserRole+4 EDITOR = Qt.UserRole+5 IMAGE = Qt.DecorationRole class comic_page_delegate(QStyledItemDelegate): def __init__(self, parent=None): super(QStyledItemDelegate, self).__init__(parent) def paint(self, painter, option, index): if (index.isValid() == False): return painter.save() painter.setOpacity(0.6) if(option.state & QStyle.State_Selected): painter.fillRect(option.rect, option.palette.highlight()) if (option.state & QStyle.State_MouseOver): painter.setOpacity(0.25) painter.fillRect(option.rect, option.palette.highlight()) painter.setOpacity(1.0) painter.setFont(option.font) metrics = QFontMetrics(option.font) regular = QFont(option.font) italics = QFont(option.font) italics.setItalic(True) icon = QIcon(index.data(CPE.IMAGE)) rect = option.rect margin = 4 decoratonSize = QSize(option.decorationSize) imageSize = icon.actualSize(option.decorationSize) leftSideThumbnail = (decoratonSize.width()-imageSize.width())/2 if (rect.width() < decoratonSize.width()): leftSideThumbnail = max(0, (rect.width()-imageSize.width())/2) topSizeThumbnail = ((rect.height()-imageSize.height())/2)+rect.top() painter.drawImage(QRect(leftSideThumbnail, topSizeThumbnail, imageSize.width(), imageSize.height()), icon.pixmap(imageSize).toImage()) labelWidth = rect.width()-decoratonSize.width()-(margin*3) if (decoratonSize.width()+(margin*2)< rect.width()): textRect = QRect(decoratonSize.width()+margin, margin+rect.top(), labelWidth, metrics.height()) textTitle = metrics.elidedText(str(index.row()+1)+". "+index.data(CPE.TITLE), Qt.ElideRight, labelWidth) painter.drawText(textRect, Qt.TextWordWrap, textTitle) if rect.height()/(metrics.lineSpacing()+margin) > 5 or index.data(CPE.KEYWORDS) is not None: painter.setOpacity(0.6) textRect = QRect(textRect.left(), textRect.bottom()+margin, labelWidth, metrics.height()) if textRect.bottom() < rect.bottom(): textKeyWords = index.data(CPE.KEYWORDS) if textKeyWords == None: textKeyWords = i18n("No keywords") painter.setOpacity(0.3) painter.setFont(italics) textKeyWords = metrics.elidedText(textKeyWords, Qt.ElideRight, labelWidth) painter.drawText(textRect, Qt.TextWordWrap, textKeyWords) painter.setFont(regular) if rect.height()/(metrics.lineSpacing()+margin) > 3: painter.setOpacity(0.6) textRect = QRect(textRect.left(), textRect.bottom()+margin, labelWidth, metrics.height()) if textRect.bottom()+metrics.height() < rect.bottom(): textLastEdit = index.data(CPE.LASTEDIT) if textLastEdit is None: textLastEdit = i18n("No last edit timestamp") if index.data(CPE.EDITOR) is not None: textLastEdit += " - " + index.data(CPE.EDITOR) if (index.data(CPE.LASTEDIT) is None) and (index.data(CPE.EDITOR) is None): painter.setOpacity(0.3) painter.setFont(italics) textLastEdit = metrics.elidedText(textLastEdit, Qt.ElideRight, labelWidth) painter.drawText(textRect, Qt.TextWordWrap, textLastEdit) painter.setFont(regular) descRect = QRect(textRect.left(), textRect.bottom()+margin, labelWidth, (rect.bottom()-margin) - (textRect.bottom()+margin)) if textRect.bottom()+metrics.height() < rect.bottom(): textRect.setBottom(textRect.bottom()+(margin/2)) textRect.setLeft(textRect.left()-(margin/2)) painter.setOpacity(0.4) painter.drawLine(textRect.bottomLeft(), textRect.bottomRight()) painter.setOpacity(1.0) textDescription = index.data(CPE.DESCRIPTION) if textDescription is None: textDescription = i18n("No description") painter.setOpacity(0.3) painter.setFont(italics) linesTotal = floor(descRect.height()/metrics.lineSpacing()) if linesTotal == 1: textDescription = metrics.elidedText(textDescription, Qt.ElideRight, labelWidth) painter.drawText(descRect, Qt.TextWordWrap, textDescription) else: descRect.setHeight(linesTotal*metrics.lineSpacing()) totalDescHeight = metrics.boundingRect(descRect, Qt.TextWordWrap, textDescription).height() if totalDescHeight>descRect.height(): if totalDescHeight-metrics.lineSpacing()>descRect.height(): painter.setOpacity(0.5) painter.drawText(descRect, Qt.TextWordWrap, textDescription) descRect.setHeight((linesTotal-1)*metrics.lineSpacing()) painter.drawText(descRect, Qt.TextWordWrap, textDescription) descRect.setHeight((linesTotal-2)*metrics.lineSpacing()) painter.drawText(descRect, Qt.TextWordWrap, textDescription) else: painter.setOpacity(0.75) painter.drawText(descRect, Qt.TextWordWrap, textDescription) descRect.setHeight((linesTotal-1)*metrics.lineSpacing()) painter.drawText(descRect, Qt.TextWordWrap, textDescription) else: painter.drawText(descRect, Qt.TextWordWrap, textDescription) painter.setFont(regular) painter.restore() """ This is a Krita docker called 'Comics Manager'. It allows people to create comics project files, load those files, add pages, remove pages, move pages, manage the metadata, and finally export the result. The logic behind this docker is that it is very easy to get lost in a comics project due to the massive amount of files. By having a docker that gives the user quick access to the pages and also allows them to do all of the meta-stuff, like meta data, but also reordering the pages, the chaos of managing the project should take up less time, and more time can be focused on actual writing and drawing. """ class comics_project_manager_docker(DockWidget): setupDictionary = {} stringName = i18n("Comics Manager") projecturl = None def __init__(self): super().__init__() self.setWindowTitle(self.stringName) # Setup layout: base = QHBoxLayout() widget = QWidget() widget.setLayout(base) baseLayout = QSplitter() base.addWidget(baseLayout) self.setWidget(widget) buttonLayout = QVBoxLayout() buttonBox = QWidget() buttonBox.setLayout(buttonLayout) baseLayout.addWidget(buttonBox) # Comic page list and pages model self.comicPageList = QListView() self.comicPageList.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.comicPageList.setDragEnabled(True) self.comicPageList.setDragDropMode(QAbstractItemView.InternalMove) self.comicPageList.setDefaultDropAction(Qt.MoveAction) self.comicPageList.setAcceptDrops(True) self.comicPageList.setItemDelegate(comic_page_delegate()) self.pagesModel = QStandardItemModel() self.comicPageList.doubleClicked.connect(self.slot_open_page) self.comicPageList.setIconSize(QSize(128, 128)) # self.comicPageList.itemDelegate().closeEditor.connect(self.slot_write_description) self.pagesModel.layoutChanged.connect(self.slot_write_config) self.pagesModel.rowsInserted.connect(self.slot_write_config) self.pagesModel.rowsRemoved.connect(self.slot_write_config) self.pagesModel.rowsMoved.connect(self.slot_write_config) self.comicPageList.setModel(self.pagesModel) pageBox = QWidget() pageBox.setLayout(QVBoxLayout()) zoomSlider = QSlider(Qt.Horizontal, None) zoomSlider.setRange(1, 8) zoomSlider.setValue(4) zoomSlider.setTickInterval(1) zoomSlider.setMinimumWidth(10) zoomSlider.valueChanged.connect(self.slot_scale_thumbnails) self.projectName = Elided_Text_Label() pageBox.layout().addWidget(self.projectName) pageBox.layout().addWidget(zoomSlider) pageBox.layout().addWidget(self.comicPageList) baseLayout.addWidget(pageBox) self.btn_project = QToolButton() self.btn_project.setPopupMode(QToolButton.MenuButtonPopup) self.btn_project.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) menu_project = QMenu() self.action_new_project = QAction(i18n("New Project"), self) self.action_new_project.triggered.connect(self.slot_new_project) self.action_load_project = QAction(i18n("Open Project"), self) self.action_load_project.triggered.connect(self.slot_open_config) menu_project.addAction(self.action_new_project) menu_project.addAction(self.action_load_project) self.btn_project.setMenu(menu_project) self.btn_project.setDefaultAction(self.action_load_project) buttonLayout.addWidget(self.btn_project) # Settings dropdown with actions for the different settings menus. self.btn_settings = QToolButton() self.btn_settings.setPopupMode(QToolButton.MenuButtonPopup) self.btn_settings.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.action_edit_project_settings = QAction(i18n("Project Settings"), self) self.action_edit_project_settings.triggered.connect(self.slot_edit_project_settings) self.action_edit_meta_data = QAction(i18n("Meta Data"), self) self.action_edit_meta_data.triggered.connect(self.slot_edit_meta_data) self.action_edit_export_settings = QAction(i18n("Export Settings"), self) self.action_edit_export_settings.triggered.connect(self.slot_edit_export_settings) menu_settings = QMenu() menu_settings.addAction(self.action_edit_project_settings) menu_settings.addAction(self.action_edit_meta_data) menu_settings.addAction(self.action_edit_export_settings) self.btn_settings.setDefaultAction(self.action_edit_project_settings) self.btn_settings.setMenu(menu_settings) buttonLayout.addWidget(self.btn_settings) self.btn_settings.setDisabled(True) # Add page drop down with different page actions. self.btn_add_page = QToolButton() self.btn_add_page.setPopupMode(QToolButton.MenuButtonPopup) self.btn_add_page.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.action_add_page = QAction(i18n("Add Page"), self) self.action_add_page.triggered.connect(self.slot_add_new_page_single) self.action_add_template = QAction(i18n("Add Page from Template"), self) self.action_add_template.triggered.connect(self.slot_add_new_page_from_template) self.action_add_existing = QAction(i18n("Add Existing Pages"), self) self.action_add_existing.triggered.connect(self.slot_add_page_from_url) self.action_remove_selected_page = QAction(i18n("Remove Page"), self) self.action_remove_selected_page.triggered.connect(self.slot_remove_selected_page) self.action_resize_all_pages = QAction(i18n("Batch Resize"), self) self.action_resize_all_pages.triggered.connect(self.slot_batch_resize) self.btn_add_page.setDefaultAction(self.action_add_page) self.action_show_page_viewer = QAction(i18n("View Page In Window"), self) self.action_show_page_viewer.triggered.connect(self.slot_show_page_viewer) self.action_scrape_authors = QAction(i18n("Scrape Author Info"), self) self.action_scrape_authors.setToolTip(i18n("Search for author information in documents and add it to the author list. This does not check for duplicates.")) self.action_scrape_authors.triggered.connect(self.slot_scrape_author_list) self.action_scrape_translations = QAction(i18n("Scrape Text for Translation"), self) self.action_scrape_translations.triggered.connect(self.slot_scrape_translations) actionList = [] menu_page = QMenu() actionList.append(self.action_add_page) actionList.append(self.action_add_template) actionList.append(self.action_add_existing) actionList.append(self.action_remove_selected_page) actionList.append(self.action_resize_all_pages) actionList.append(self.action_show_page_viewer) actionList.append(self.action_scrape_authors) actionList.append(self.action_scrape_translations) menu_page.addActions(actionList) self.btn_add_page.setMenu(menu_page) buttonLayout.addWidget(self.btn_add_page) self.btn_add_page.setDisabled(True) self.comicPageList.setContextMenuPolicy(Qt.ActionsContextMenu) self.comicPageList.addActions(actionList) # Export button that... exports. self.btn_export = QPushButton(i18n("Export Comic")) self.btn_export.clicked.connect(self.slot_export) buttonLayout.addWidget(self.btn_export) self.btn_export.setDisabled(True) self.btn_project_url = QPushButton(i18n("Copy Location")) self.btn_project_url.setToolTip(i18n("Copies the path of the project to the clipboard. Useful for quickly copying to a file manager or the like.")) self.btn_project_url.clicked.connect(self.slot_copy_project_url) self.btn_project_url.setDisabled(True) buttonLayout.addWidget(self.btn_project_url) self.page_viewer_dialog = comics_project_page_viewer.comics_project_page_viewer() Application.notifier().imageSaved.connect(self.slot_check_for_page_update) buttonLayout.addItem(QSpacerItem(0, 0, QSizePolicy.Minimum, QSizePolicy.MinimumExpanding)) """ Open the config file and load the json file into a dictionary. """ def slot_open_config(self): self.path_to_config = QFileDialog.getOpenFileName(caption=i18n("Please select the JSON comic config file."), filter=str(i18n("JSON files") + "(*.json)"))[0] if os.path.exists(self.path_to_config) is True: configFile = open(self.path_to_config, "r", newline="", encoding="utf-16") self.setupDictionary = json.load(configFile) self.projecturl = os.path.dirname(str(self.path_to_config)) configFile.close() self.load_config() """ Further config loading. """ def load_config(self): self.projectName.setMainText(text=str(self.setupDictionary["projectName"])) self.fill_pages() self.btn_settings.setEnabled(True) self.btn_add_page.setEnabled(True) self.btn_export.setEnabled(True) self.btn_project_url.setEnabled(True) """ Fill the pages model with the pages from the pages list. """ def fill_pages(self): self.loadingPages = True self.pagesModel.clear() pagesList = [] if "pages" in self.setupDictionary.keys(): pagesList = self.setupDictionary["pages"] progress = QProgressDialog() progress.setMinimum(0) progress.setMaximum(len(pagesList)) progress.setWindowTitle(i18n("Loading Pages...")) for url in pagesList: absurl = os.path.join(self.projecturl, url) if (os.path.exists(absurl)): #page = Application.openDocument(absurl) page = zipfile.ZipFile(absurl, "r") thumbnail = QImage.fromData(page.read("preview.png")) pageItem = QStandardItem() dataList = self.get_description_and_title(page.read("documentinfo.xml")) if (dataList[0].isspace() or len(dataList[0]) < 1): dataList[0] = os.path.basename(url) pageItem.setText(dataList[0].replace("_", " ")) pageItem.setDragEnabled(True) pageItem.setDropEnabled(False) pageItem.setEditable(False) pageItem.setIcon(QIcon(QPixmap.fromImage(thumbnail))) pageItem.setData(dataList[1], role = CPE.DESCRIPTION) pageItem.setData(url, role = CPE.URL) pageItem.setData(dataList[2], role = CPE.KEYWORDS) pageItem.setData(dataList[3], role = CPE.LASTEDIT) pageItem.setData(dataList[4], role = CPE.EDITOR) pageItem.setToolTip(url) page.close() self.pagesModel.appendRow(pageItem) progress.setValue(progress.value() + 1) progress.setValue(len(pagesList)) self.loadingPages = False """ Function that is triggered by the zoomSlider Resizes the thumbnails. """ def slot_scale_thumbnails(self, multiplier=4): self.comicPageList.setIconSize(QSize(multiplier * 32, multiplier * 32)) """ Function that takes the documentinfo.xml and parses it for the title, subject and abstract tags, to get the title and description. @returns a stringlist with the name on 0 and the description on 1. """ def get_description_and_title(self, string): xmlDoc = ET.fromstring(string) calligra = str("{http://www.calligra.org/DTD/document-info}") name = "" if ET.iselement(xmlDoc[0].find(calligra + 'title')): name = xmlDoc[0].find(calligra + 'title').text if name is None: name = " " desc = "" if ET.iselement(xmlDoc[0].find(calligra + 'subject')): desc = xmlDoc[0].find(calligra + 'subject').text if desc is None or desc.isspace() or len(desc) < 1: if ET.iselement(xmlDoc[0].find(calligra + 'abstract')): desc = xmlDoc[0].find(calligra + 'abstract').text if desc is not None: if desc.startswith(""): desc = desc[:-len("]]>")] keywords = "" if ET.iselement(xmlDoc[0].find(calligra + 'keyword')): keywords = xmlDoc[0].find(calligra + 'keyword').text date = "" if ET.iselement(xmlDoc[0].find(calligra + 'date')): date = xmlDoc[0].find(calligra + 'date').text author = [] if ET.iselement(xmlDoc[1].find(calligra + 'creator-first-name')): string = xmlDoc[1].find(calligra + 'creator-first-name').text if string is not None: author.append(string) if ET.iselement(xmlDoc[1].find(calligra + 'creator-last-name')): string = xmlDoc[1].find(calligra + 'creator-last-name').text if string is not None: author.append(string) if ET.iselement(xmlDoc[1].find(calligra + 'full-name')): string = xmlDoc[1].find(calligra + 'full-name').text if string is not None: author.append(string) return [name, desc, keywords, date, " ".join(author)] """ Scrapes authors from the author data in the document info and puts them into the author list. Doesn't check for duplicates. """ def slot_scrape_author_list(self): listOfAuthors = [] if "authorList" in self.setupDictionary.keys(): listOfAuthors = self.setupDictionary["authorList"] if "pages" in self.setupDictionary.keys(): for relurl in self.setupDictionary["pages"]: absurl = os.path.join(self.projecturl, relurl) page = zipfile.ZipFile(absurl, "r") xmlDoc = ET.fromstring(page.read("documentinfo.xml")) calligra = str("{http://www.calligra.org/DTD/document-info}") authorelem = xmlDoc.find(calligra + 'author') author = {} if ET.iselement(authorelem.find(calligra + 'full-name')): author["nickname"] = str(authorelem.find(calligra + 'full-name').text) if ET.iselement(authorelem.find(calligra + 'creator-first-name')): author["first-name"] = str(authorelem.find(calligra + 'creator-first-name').text) if ET.iselement(authorelem.find(calligra + 'initial')): author["initials"] = str(authorelem.find(calligra + 'initial').text) if ET.iselement(authorelem.find(calligra + 'creator-last-name')): author["last-name"] = str(authorelem.find(calligra + 'creator-last-name').text) if ET.iselement(authorelem.find(calligra + 'email')): author["email"] = str(authorelem.find(calligra + 'email').text) if ET.iselement(authorelem.find(calligra + 'contact')): contact = authorelem.find(calligra + 'contact') contactMode = contact.get("type") if contactMode == "email": author["email"] = str(contact.text) if contactMode == "homepage": author["homepage"] = str(contact.text) if ET.iselement(authorelem.find(calligra + 'position')): author["role"] = str(authorelem.find(calligra + 'position').text) listOfAuthors.append(author) page.close() self.setupDictionary["authorList"] = listOfAuthors """ Edit the general project settings like the project name, concept, pages location, export location, template location, metadata """ def slot_edit_project_settings(self): dialog = comics_project_settings_dialog.comics_project_details_editor(self.projecturl) dialog.setConfig(self.setupDictionary, self.projecturl) if dialog.exec_() == QDialog.Accepted: self.setupDictionary = dialog.getConfig(self.setupDictionary) self.slot_write_config() self.projectName.setMainText(str(self.setupDictionary["projectName"])) """ This allows users to select existing pages and add them to the pages list. The pages are currently not copied to the pages folder. Useful for existing projects. """ def slot_add_page_from_url(self): # get the pages. urlList = QFileDialog.getOpenFileNames(caption=i18n("Which existing pages to add?"), directory=self.projecturl, filter=str(i18n("Krita files") + "(*.kra)"))[0] # get the existing pages list. pagesList = [] if "pages" in self.setupDictionary.keys(): pagesList = self.setupDictionary["pages"] # And add each url in the url list to the pages list and the model. for url in urlList: if self.projecturl not in urlList: newUrl = os.path.join(self.projecturl, self.setupDictionary["pagesLocation"], os.path.basename(url)) shutil.move(url, newUrl) url = newUrl relative = os.path.relpath(url, self.projecturl) if url not in pagesList: page = zipfile.ZipFile(url, "r") thumbnail = QImage.fromData(page.read("preview.png")) dataList = self.get_description_and_title(page.read("documentinfo.xml")) if (dataList[0].isspace() or len(dataList[0]) < 1): dataList[0] = os.path.basename(url) newPageItem = QStandardItem() newPageItem.setIcon(QIcon(QPixmap.fromImage(thumbnail))) newPageItem.setDragEnabled(True) newPageItem.setDropEnabled(False) newPageItem.setEditable(False) newPageItem.setText(dataList[0].replace("_", " ")) newPageItem.setData(dataList[1], role = CPE.DESCRIPTION) newPageItem.setData(relative, role = CPE.URL) newPageItem.setData(dataList[2], role = CPE.KEYWORDS) newPageItem.setData(dataList[3], role = CPE.LASTEDIT) newPageItem.setData(dataList[4], role = CPE.EDITOR) newPageItem.setToolTip(relative) page.close() self.pagesModel.appendRow(newPageItem) """ Remove the selected page from the list of pages. This does not remove it from disk(far too dangerous). """ def slot_remove_selected_page(self): index = self.comicPageList.currentIndex() self.pagesModel.removeRow(index.row()) """ This function adds a new page from the default template. If there's no default template, or the file does not exist, it will show the create/import template dialog. It will remember the selected item as the default template. """ def slot_add_new_page_single(self): templateUrl = "templatepage" templateExists = False if "singlePageTemplate" in self.setupDictionary.keys(): templateUrl = self.setupDictionary["singlePageTemplate"] if os.path.exists(os.path.join(self.projecturl, templateUrl)): templateExists = True if templateExists is False: if "templateLocation" not in self.setupDictionary.keys(): self.setupDictionary["templateLocation"] = os.path.relpath(QFileDialog.getExistingDirectory(caption=i18n("Where are the templates located?"), options=QFileDialog.ShowDirsOnly), self.projecturl) templateDir = os.path.join(self.projecturl, self.setupDictionary["templateLocation"]) template = comics_template_dialog.comics_template_dialog(templateDir) if template.exec_() == QDialog.Accepted: templateUrl = os.path.relpath(template.url(), self.projecturl) self.setupDictionary["singlePageTemplate"] = templateUrl if os.path.exists(os.path.join(self.projecturl, templateUrl)): self.add_new_page(templateUrl) """ This function always asks for a template showing the new template window. This allows users to have multiple different templates created for back covers, spreads, other and have them accessible, while still having the convenience of a singular "add page" that adds a default. """ def slot_add_new_page_from_template(self): if "templateLocation" not in self.setupDictionary.keys(): self.setupDictionary["templateLocation"] = os.path.relpath(QFileDialog.getExistingDirectory(caption=i18n("Where are the templates located?"), options=QFileDialog.ShowDirsOnly), self.projecturl) templateDir = os.path.join(self.projecturl, self.setupDictionary["templateLocation"]) template = comics_template_dialog.comics_template_dialog(templateDir) if template.exec_() == QDialog.Accepted: templateUrl = os.path.relpath(template.url(), self.projecturl) self.add_new_page(templateUrl) """ This is the actual function that adds the template using the template url. It will attempt to name the new page projectName+number. """ def add_new_page(self, templateUrl): # check for page list and or location. pagesList = [] if "pages" in self.setupDictionary.keys(): pagesList = self.setupDictionary["pages"] if not "pageNumber" in self.setupDictionary.keys(): self.setupDictionary['pageNumber'] = 0 if (str(self.setupDictionary["pagesLocation"]).isspace()): self.setupDictionary["pagesLocation"] = os.path.relpath(QFileDialog.getExistingDirectory(caption=i18n("Where should the pages go?"), options=QFileDialog.ShowDirsOnly), self.projecturl) # Search for the possible name. extraUnderscore = str() if str(self.setupDictionary["projectName"])[-1].isdigit(): extraUnderscore = "_" self.setupDictionary['pageNumber'] += 1 pageName = str(self.setupDictionary["projectName"]).replace(" ", "_") + extraUnderscore + str(format(self.setupDictionary['pageNumber'], "03d")) url = os.path.join(str(self.setupDictionary["pagesLocation"]), pageName + ".kra") # open the page by opening the template and resaving it, or just opening it. absoluteUrl = os.path.join(self.projecturl, url) if (os.path.exists(absoluteUrl)): newPage = Application.openDocument(absoluteUrl) else: booltemplateExists = os.path.exists(os.path.join(self.projecturl, templateUrl)) if booltemplateExists is False: templateUrl = os.path.relpath(QFileDialog.getOpenFileName(caption=i18n("Which image should be the basis the new page?"), directory=self.projecturl, filter=str(i18n("Krita files") + "(*.kra)"))[0], self.projecturl) newPage = Application.openDocument(os.path.join(self.projecturl, templateUrl)) newPage.waitForDone() newPage.setFileName(absoluteUrl) newPage.setName(pageName.replace("_", " ")) newPage.save() newPage.waitForDone() # Get out the extra data for the standard item. newPageItem = QStandardItem() newPageItem.setIcon(QIcon(QPixmap.fromImage(newPage.thumbnail(256, 256)))) newPageItem.setDragEnabled(True) newPageItem.setDropEnabled(False) newPageItem.setEditable(False) newPageItem.setText(pageName.replace("_", " ")) newPageItem.setData("", role = CPE.DESCRIPTION) newPageItem.setData(url, role = CPE.URL) newPageItem.setData("", role = CPE.KEYWORDS) newPageItem.setData("", role = CPE.LASTEDIT) newPageItem.setData("", role = CPE.EDITOR) newPageItem.setToolTip(url) # close page document. while os.path.exists(absoluteUrl) is False: qApp.processEvents() newPage.close() # add item to page. self.pagesModel.appendRow(newPageItem) """ - Write to the json configuratin file. + Write to the json configuration file. This also checks the current state of the pages list. """ def slot_write_config(self): # Don't load when the pages are still being loaded, otherwise we'll be overwriting our own pages list. if (self.loadingPages is False): print("CPMT: writing comic configuration...") # Generate a pages list from the pagesmodel. pagesList = [] for i in range(self.pagesModel.rowCount()): index = self.pagesModel.index(i, 0) url = str(self.pagesModel.data(index, role=CPE.URL)) if url not in pagesList: pagesList.append(url) self.setupDictionary["pages"] = pagesList # Save to our json file. configFile = open(self.path_to_config, "w", newline="", encoding="utf-16") json.dump(self.setupDictionary, configFile, indent=4, sort_keys=True, ensure_ascii=False) configFile.close() print("CPMT: done") """ Open a page in the pagesmodel in Krita. """ def slot_open_page(self, index): if index.column() is 0: # Get the absolute url from the relative one in the pages model. absoluteUrl = os.path.join(self.projecturl, str(self.pagesModel.data(index, role=CPE.URL))) # Make sure the page exists. if os.path.exists(absoluteUrl): page = Application.openDocument(absoluteUrl) # Set the title to the filename if it was empty. It looks a bit neater. if page.name().isspace or len(page.name()) < 1: page.setName(str(self.pagesModel.data(index, role=Qt.DisplayRole)).replace("_", " ")) # Add views for the document so the user can use it. Application.activeWindow().addView(page) Application.setActiveDocument(page) else: print("CPMT: The page cannot be opened because the file doesn't exist:", absoluteUrl) """ Call up the metadata editor dialog. Only when the dialog is "Accepted" will the metadata be saved. """ def slot_edit_meta_data(self): dialog = comics_metadata_dialog.comic_meta_data_editor() dialog.setConfig(self.setupDictionary) if (dialog.exec_() == QDialog.Accepted): self.setupDictionary = dialog.getConfig(self.setupDictionary) self.slot_write_config() """ - An attempt at making the description editable from the comic pages list. It is currently not working because ZipFile - has no overwrite mechanism, and I don't have the energy to write one yet. + An attempt at making the description editable from the comic pages list. + It is currently not working because ZipFile has no overwrite mechanism, + and I don't have the energy to write one yet. """ def slot_write_description(self, index): for row in range(self.pagesModel.rowCount()): index = self.pagesModel.index(row, 1) indexUrl = self.pagesModel.index(row, 0) absoluteUrl = os.path.join(self.projecturl, str(self.pagesModel.data(indexUrl, role=CPE.URL))) page = zipfile.ZipFile(absoluteUrl, "a") xmlDoc = ET.ElementTree() ET.register_namespace("", "http://www.calligra.org/DTD/document-info") location = os.path.join(self.projecturl, "documentinfo.xml") xmlDoc.parse(location) xmlroot = ET.fromstring(page.read("documentinfo.xml")) calligra = "{http://www.calligra.org/DTD/document-info}" aboutelem = xmlroot.find(calligra + 'about') if ET.iselement(aboutelem.find(calligra + 'subject')): desc = aboutelem.find(calligra + 'subject') desc.text = self.pagesModel.data(index, role=Qt.EditRole) xmlstring = ET.tostring(xmlroot, encoding='unicode', method='xml', short_empty_elements=False) page.writestr(zinfo_or_arcname="documentinfo.xml", data=xmlstring) for document in Application.documents(): if str(document.fileName()) == str(absoluteUrl): document.setDocumentInfo(xmlstring) page.close() """ Calls up the export settings dialog. Only when accepted will the configuration be written. """ def slot_edit_export_settings(self): dialog = comics_export_dialog.comic_export_setting_dialog() dialog.setConfig(self.setupDictionary) if (dialog.exec_() == QDialog.Accepted): self.setupDictionary = dialog.getConfig(self.setupDictionary) self.slot_write_config() """ Export the comic. Won't work without export settings set. """ def slot_export(self): exporter = comics_exporter.comicsExporter() exporter.set_config(self.setupDictionary, self.projecturl) exportSuccess = exporter.export() if exportSuccess: print("CPMT: Export success! The files have been written to the export folder!") QMessageBox.information(self, i18n("Export success"), i18n("The files have been written to the export folder."), QMessageBox.Ok) """ Calls up the comics project setup wizard so users can create a new json file with the basic information. """ def slot_new_project(self): setup = comics_project_setup_wizard.ComicsProjectSetupWizard() setup.showDialog() """ This is triggered by any document save. It checks if the given url in in the pages list, and if so, updates the appropriate page thumbnail. This helps with the management of the pages, because the user will be able to see the thumbnails as a todo for the whole comic, giving a good overview over whether they still need to ink, color or the like for a given page, and it thus also rewards the user whenever they save. """ def slot_check_for_page_update(self, url): if "pages" in self.setupDictionary.keys(): relUrl = os.path.relpath(url, self.projecturl) if relUrl in self.setupDictionary["pages"]: index = self.pagesModel.index(self.setupDictionary["pages"].index(relUrl), 0) if index.isValid(): pageItem = self.pagesModel.itemFromIndex(index) page = zipfile.ZipFile(url, "r") dataList = self.get_description_and_title(page.read("documentinfo.xml")) if (dataList[0].isspace() or len(dataList[0]) < 1): dataList[0] = os.path.basename(url) thumbnail = QImage.fromData(page.read("preview.png")) pageItem.setIcon(QIcon(QPixmap.fromImage(thumbnail))) pageItem.setText(dataList[0]) pageItem.setData(dataList[1], role = CPE.DESCRIPTION) pageItem.setData(url, role = CPE.URL) pageItem.setData(dataList[2], role = CPE.KEYWORDS) pageItem.setData(dataList[3], role = CPE.LASTEDIT) pageItem.setData(dataList[4], role = CPE.EDITOR) self.pagesModel.setItem(index.row(), index.column(), pageItem) """ Resize all the pages in the pages list. - It will show a dialog with the options for resizing. Then, it will try to pop up a progress dialog while resizing. + It will show a dialog with the options for resizing. + Then, it will try to pop up a progress dialog while resizing. The progress dialog shows the remaining time and pages. """ def slot_batch_resize(self): dialog = QDialog() dialog.setWindowTitle(i18n("Resize all Pages")) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(dialog.accept) buttons.rejected.connect(dialog.reject) sizesBox = comics_export_dialog.comic_export_resize_widget("Scale", batch=True, fileType=False) exporterSizes = comics_exporter.sizesCalculator() dialog.setLayout(QVBoxLayout()) dialog.layout().addWidget(sizesBox) dialog.layout().addWidget(buttons) if dialog.exec_() == QDialog.Accepted: progress = QProgressDialog(i18n("Resizing pages..."), str(), 0, len(self.setupDictionary["pages"])) progress.setWindowTitle(i18n("Resizing Pages")) progress.setCancelButton(None) timer = QElapsedTimer() timer.start() config = {} config = sizesBox.get_config(config) for p in range(len(self.setupDictionary["pages"])): absoluteUrl = os.path.join(self.projecturl, self.setupDictionary["pages"][p]) progress.setValue(p) timePassed = timer.elapsed() if (p > 0): timeEstimated = (len(self.setupDictionary["pages"]) - p) * (timePassed / p) passedString = str(int(timePassed / 60000)) + ":" + format(int(timePassed / 1000), "02d") + ":" + format(timePassed % 1000, "03d") estimatedString = str(int(timeEstimated / 60000)) + ":" + format(int(timeEstimated / 1000), "02d") + ":" + format(int(timeEstimated % 1000), "03d") progress.setLabelText(str(i18n("{pages} of {pagesTotal} done. \nTime passed: {passedString}:\n Estimated:{estimated}")).format(pages=p, pagesTotal=len(self.setupDictionary["pages"]), passedString=passedString, estimated=estimatedString)) qApp.processEvents() if os.path.exists(absoluteUrl): doc = Application.openDocument(absoluteUrl) listScales = exporterSizes.get_scale_from_resize_config(config["Scale"], [doc.width(), doc.height(), doc.resolution(), doc.resolution()]) doc.scaleImage(listScales[0], listScales[1], listScales[2], listScales[3], "bicubic") doc.waitForDone() doc.save() doc.waitForDone() doc.close() def slot_show_page_viewer(self): index = int(self.comicPageList.currentIndex().row()) self.page_viewer_dialog.load_comic(self.path_to_config) self.page_viewer_dialog.go_to_page_index(index) self.page_viewer_dialog.show() """ Function to copy the current project location into the clipboard. This is useful for users because they'll be able to use that url to quickly move to the project location in outside applications. """ def slot_copy_project_url(self): if self.projecturl is not None: clipboard = qApp.clipboard() clipboard.setText(str(self.projecturl)) """ Scrape text files with the textlayer keys for text, and put those in a POT file. This makes it possible to handle translations. """ def slot_scrape_translations(self): translationFolder = self.setupDictionary.get("translationLocation", "translations") fullTranslationPath = os.path.join(self.projecturl, translationFolder) os.makedirs(fullTranslationPath, exist_ok=True) textLayersToSearch = self.setupDictionary.get("textLayerNames", ["text"]) scraper = comics_project_translation_scraper.translation_scraper(self.projecturl, translationFolder, textLayersToSearch, self.setupDictionary["projectName"]) # Run text scraper. language = self.setupDictionary.get("language", "en") metadata = {} metadata["title"] = self.setupDictionary.get("title", "") metadata["summary"] = self.setupDictionary.get("summary", "") metadata["keywords"] = ", ".join(self.setupDictionary.get("otherKeywords", [""])) metadata["transnotes"] = self.setupDictionary.get("translatorHeader", "Translator's Notes") scraper.start(self.setupDictionary["pages"], language, metadata) QMessageBox.information(self, i18n("Scraping success"), str(i18n("POT file has been written to: {file}")).format(file=fullTranslationPath), QMessageBox.Ok) """ This is required by the dockwidget class, otherwise unused. """ def canvasChanged(self, canvas): pass """ Add docker to program """ Application.addDockWidgetFactory(DockWidgetFactory("comics_project_manager_docker", DockWidgetFactoryBase.DockRight, comics_project_manager_docker)) diff --git a/plugins/tools/basictools/kis_tool_gradient.cc b/plugins/tools/basictools/kis_tool_gradient.cc index 0354591780..1c620b7da6 100644 --- a/plugins/tools/basictools/kis_tool_gradient.cc +++ b/plugins/tools/basictools/kis_tool_gradient.cc @@ -1,304 +1,304 @@ /* * kis_tool_gradient.cc - part of Krita * * Copyright (c) 2002 Patrick Julien * Copyright (c) 2003 Boudewijn Rempt * Copyright (c) 2004-2007 Adrian Page * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_tool_gradient.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_resources_snapshot.h" KisToolGradient::KisToolGradient(KoCanvasBase * canvas) : KisToolPaint(canvas, KisCursor::load("tool_gradient_cursor.png", 6, 6)) { setObjectName("tool_gradient"); m_startPos = QPointF(0, 0); m_endPos = QPointF(0, 0); m_reverse = false; m_shape = KisGradientPainter::GradientShapeLinear; m_repeat = KisGradientPainter::GradientRepeatNone; m_antiAliasThreshold = 0.2; } KisToolGradient::~KisToolGradient() { } void KisToolGradient::resetCursorStyle() { KisToolPaint::resetCursorStyle(); overrideCursorIfNotEditable(); } void KisToolGradient::activate(ToolActivation toolActivation, const QSet &shapes) { KisToolPaint::activate(toolActivation, shapes); m_configGroup = KSharedConfig::openConfig()->group(toolId()); } void KisToolGradient::paint(QPainter &painter, const KoViewConverter &converter) { if (mode() == KisTool::PAINT_MODE && m_startPos != m_endPos) { qreal sx, sy; converter.zoom(&sx, &sy); painter.scale(sx / currentImage()->xRes(), sy / currentImage()->yRes()); paintLine(painter); } } void KisToolGradient::beginPrimaryAction(KoPointerEvent *event) { if (!nodeEditable()) { event->ignore(); return; } setMode(KisTool::PAINT_MODE); m_startPos = convertToPixelCoordAndSnap(event, QPointF(), false); m_endPos = m_startPos; } void KisToolGradient::continuePrimaryAction(KoPointerEvent *event) { /** * TODO: The gradient tool is still not in strokes, so the end of * its action can call processEvent(), which would result in * nested event hadler calls. Please uncomment this line * when the tool is ported to strokes. */ //CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE); QPointF pos = convertToPixelCoordAndSnap(event, QPointF(), false); QRectF bound(m_startPos, m_endPos); canvas()->updateCanvas(convertToPt(bound.normalized())); if (event->modifiers() == Qt::ShiftModifier) { m_endPos = straightLine(pos); } else { m_endPos = pos; } bound.setTopLeft(m_startPos); bound.setBottomRight(m_endPos); canvas()->updateCanvas(convertToPt(bound.normalized())); } void KisToolGradient::endPrimaryAction(KoPointerEvent *event) { Q_UNUSED(event); CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE); setMode(KisTool::HOVER_MODE); if (!currentNode() || !blockUntilOperationsFinished()) return; if (m_startPos == m_endPos) { return; } KisPaintDeviceSP device; KisImageSP image = this->image(); KisResourcesSnapshotSP resources = new KisResourcesSnapshot(image, currentNode(), this->canvas()->resourceManager()); if (image && (device = resources->currentNode()->paintDevice())) { QApplication::setOverrideCursor(Qt::BusyCursor); KUndo2MagicString actionName = kundo2_i18n("Gradient"); KisUndoAdapter *undoAdapter = image->undoAdapter(); undoAdapter->beginMacro(actionName); KisGradientPainter painter(device, resources->activeSelection()); resources->setupPainter(&painter); painter.beginTransaction(); KisCanvas2 * canvas = dynamic_cast(this->canvas()); KoUpdaterPtr updater = canvas->viewManager()->createUnthreadedUpdater(i18nc("@info:progress", "Gradient...")); painter.setProgress(updater); painter.setGradientShape(m_shape); painter.paintGradient(m_startPos, m_endPos, m_repeat, m_antiAliasThreshold, m_reverse, 0, 0, image->width(), image->height()); painter.endTransaction(undoAdapter); undoAdapter->endMacro(); QApplication::restoreOverrideCursor(); currentNode()->setDirty(); notifyModified(); } canvas()->updateCanvas(convertToPt(currentImage()->bounds())); } QPointF KisToolGradient::straightLine(QPointF point) { QPointF comparison = point - m_startPos; QPointF result; if (fabs(comparison.x()) > fabs(comparison.y())) { result.setX(point.x()); result.setY(m_startPos.y()); } else { result.setX(m_startPos.x()); result.setY(point.y()); } return result; } void KisToolGradient::paintLine(QPainter& gc) { if (canvas()) { QPen old = gc.pen(); QPen pen(Qt::SolidLine); gc.setPen(pen); gc.drawLine(m_startPos, m_endPos); gc.setPen(old); } } QWidget* KisToolGradient::createOptionWidget() { QWidget *widget = KisToolPaint::createOptionWidget(); Q_CHECK_PTR(widget); widget->setObjectName(toolId() + " option widget"); // Make sure to create the connections last after everything is set up. The initialized values // won't be loaded from the configuration file if you add the widget before the connection m_lbShape = new QLabel(i18n("Shape:"), widget); m_cmbShape = new KComboBox(widget); m_cmbShape->setObjectName("shape_combo"); m_cmbShape->addItem(i18nc("the gradient will be drawn linearly", "Linear")); m_cmbShape->addItem(i18nc("the gradient will be drawn bilinearly", "Bi-Linear")); m_cmbShape->addItem(i18nc("the gradient will be drawn radially", "Radial")); m_cmbShape->addItem(i18nc("the gradient will be drawn in a square around a centre", "Square")); - m_cmbShape->addItem(i18nc("the gradient will be drawn as an assymmetric cone", "Conical")); + m_cmbShape->addItem(i18nc("the gradient will be drawn as an asymmetric cone", "Conical")); m_cmbShape->addItem(i18nc("the gradient will be drawn as a symmetric cone", "Conical Symmetric")); m_cmbShape->addItem(i18nc("the gradient will be drawn in a selection outline", "Shaped")); addOptionWidgetOption(m_cmbShape, m_lbShape); connect(m_cmbShape, SIGNAL(currentIndexChanged(int)), this, SLOT(slotSetShape(int))); m_lbRepeat = new QLabel(i18n("Repeat:"), widget); m_cmbRepeat = new KComboBox(widget); m_cmbRepeat->setObjectName("repeat_combo"); m_cmbRepeat->addItem(i18nc("The gradient will not repeat", "None")); m_cmbRepeat->addItem(i18nc("The gradient will repeat forwards", "Forwards")); m_cmbRepeat->addItem(i18nc("The gradient will repeat alternatingly", "Alternating")); addOptionWidgetOption(m_cmbRepeat, m_lbRepeat); connect(m_cmbRepeat, SIGNAL(currentIndexChanged(int)), this, SLOT(slotSetRepeat(int))); m_lbAntiAliasThreshold = new QLabel(i18n("Anti-alias threshold:"), widget); m_slAntiAliasThreshold = new KisDoubleSliderSpinBox(widget); m_slAntiAliasThreshold->setObjectName("threshold_slider"); m_slAntiAliasThreshold->setRange(0, 1, 3); addOptionWidgetOption(m_slAntiAliasThreshold, m_lbAntiAliasThreshold); connect(m_slAntiAliasThreshold, SIGNAL(valueChanged(qreal)), this, SLOT(slotSetAntiAliasThreshold(qreal))); m_ckReverse = new QCheckBox(i18nc("the gradient will be drawn with the color order reversed", "Reverse"), widget); m_ckReverse->setObjectName("reverse_check"); connect(m_ckReverse, SIGNAL(toggled(bool)), this, SLOT(slotSetReverse(bool))); addOptionWidgetOption(m_ckReverse); widget->setFixedHeight(widget->sizeHint().height()); // load configuration settings into widget (updating UI will update internal variables from signals/slots) m_ckReverse->setChecked((bool)m_configGroup.readEntry("reverse", false)); m_cmbShape->setCurrentIndex((int)m_configGroup.readEntry("shape", 0)); m_cmbRepeat->setCurrentIndex((int)m_configGroup.readEntry("repeat", 0)); m_slAntiAliasThreshold->setValue((qreal)m_configGroup.readEntry("antialiasThreshold", 0.0)); return widget; } void KisToolGradient::slotSetShape(int shape) { m_shape = static_cast(shape); m_configGroup.writeEntry("shape", shape); } void KisToolGradient::slotSetRepeat(int repeat) { m_repeat = static_cast(repeat); m_configGroup.writeEntry("repeat", repeat); } void KisToolGradient::slotSetReverse(bool state) { m_reverse = state; m_configGroup.writeEntry("reverse", state); } void KisToolGradient::slotSetAntiAliasThreshold(qreal value) { m_antiAliasThreshold = value; m_configGroup.writeEntry("antialiasThreshold", value); } void KisToolGradient::setOpacity(qreal opacity) { m_opacity = opacity; } diff --git a/plugins/tools/basictools/kis_tool_move.cc b/plugins/tools/basictools/kis_tool_move.cc index f41a483537..1472fc8ef2 100644 --- a/plugins/tools/basictools/kis_tool_move.cc +++ b/plugins/tools/basictools/kis_tool_move.cc @@ -1,571 +1,571 @@ /* * Copyright (c) 1999 Matthias Elter * 1999 Michael Koch * 2002 Patrick Julien * 2004 Boudewijn Rempt * 2016 Michael Abrahams * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_tool_move.h" #include #include "kis_cursor.h" #include "kis_selection.h" #include "kis_canvas2.h" #include "kis_image.h" #include "kis_tool_utils.h" #include "kis_paint_layer.h" #include "strokes/move_stroke_strategy.h" #include "kis_tool_movetooloptionswidget.h" #include "strokes/move_selection_stroke_strategy.h" #include "kis_resources_snapshot.h" #include "kis_action_registry.h" #include "krita_utils.h" #include #include #include "kis_node_manager.h" #include "kis_signals_blocker.h" KisToolMove::KisToolMove(KoCanvasBase * canvas) : KisTool(canvas, KisCursor::moveCursor()) { m_canvas = dynamic_cast(canvas); setObjectName("tool_move"); m_optionsWidget = 0; m_moveInProgress = false; QAction *a; KisActionRegistry *actionRegistry = KisActionRegistry::instance(); a = actionRegistry->makeQAction("movetool-move-up", this); addAction("movetool-move-up", a); connect(a, &QAction::triggered, [&](){moveDiscrete(MoveDirection::Up, false);}); a = actionRegistry->makeQAction("movetool-move-down", this); addAction("movetool-move-down", a); connect(a, &QAction::triggered, [&](){moveDiscrete(MoveDirection::Down, false);}); a = actionRegistry->makeQAction("movetool-move-left", this); addAction("movetool-move-left", a); connect(a, &QAction::triggered, [&](){moveDiscrete(MoveDirection::Left, false);}); a = actionRegistry->makeQAction("movetool-move-right", this); addAction("movetool-move-right", a); connect(a, &QAction::triggered, [&](){moveDiscrete(MoveDirection::Right, false);}); a = actionRegistry->makeQAction("movetool-move-up-more", this); addAction("movetool-move-up-more", a); connect(a, &QAction::triggered, [&](){moveDiscrete(MoveDirection::Up, true);}); a = actionRegistry->makeQAction("movetool-move-down-more", this); addAction("movetool-move-down-more", a); connect(a, &QAction::triggered, [&](){moveDiscrete(MoveDirection::Down, true);}); a = actionRegistry->makeQAction("movetool-move-left-more", this); addAction("movetool-move-left-more", a); connect(a, &QAction::triggered, [&](){moveDiscrete(MoveDirection::Left, true);}); a = actionRegistry->makeQAction("movetool-move-right-more", this); addAction("movetool-move-right-more", a); connect(a, &QAction::triggered, [&](){moveDiscrete(MoveDirection::Right, true);}); m_showCoordinatesAction = actionRegistry->makeQAction("movetool-show-coordinates", this); addAction("movetool-show-coordinates", m_showCoordinatesAction); } KisToolMove::~KisToolMove() { endStroke(); } void KisToolMove::resetCursorStyle() { KisTool::resetCursorStyle(); overrideCursorIfNotEditable(); } bool KisToolMove::startStrokeImpl(MoveToolMode mode, const QPoint *pos) { if (!currentNode()->isEditable()) return false; KisNodeSP node; KisNodeList nodes; KisImageSP image = this->image(); KisResourcesSnapshotSP resources = new KisResourcesSnapshot(image, currentNode(), this->canvas()->resourceManager()); KisSelectionSP selection = resources->activeSelection(); if (mode != MoveSelectedLayer && pos) { bool wholeGroup = !selection && mode == MoveGroup; node = KisToolUtils::findNode(image->root(), *pos, wholeGroup); if (node) { nodes = {node}; } } if (nodes.isEmpty()) { nodes = this->selectedNodes(); KritaUtils::filterContainer(nodes, [](KisNodeSP node) { return node->isEditable(); }); } if (nodes.size() == 1) { node = nodes.first(); } if (nodes.isEmpty()) { return false; } initHandles(nodes); /** * If the target node has changed, the stroke should be * restarted. Otherwise just continue processing current node. */ if (m_strokeId) { if (KritaUtils::compareListsUnordered(nodes, m_currentlyProcessingNodes)) { return true; } else { endStroke(); } } KisStrokeStrategy *strategy; KisPaintLayerSP paintLayer = node ? dynamic_cast(node.data()) : 0; if (paintLayer && selection && !selection->isTotallyUnselected(image->bounds())) { strategy = new MoveSelectionStrokeStrategy(paintLayer, selection, image.data(), image.data()); } else { strategy = new MoveStrokeStrategy(nodes, image.data(), image.data()); } m_strokeId = image->startStroke(strategy); m_currentlyProcessingNodes = nodes; m_accumulatedOffset = QPoint(); return true; } void KisToolMove::moveDiscrete(MoveDirection direction, bool big) { if (mode() == KisTool::PAINT_MODE) return; // Don't interact with dragging if (!currentNode()->isEditable()) return; // Don't move invisible nodes if (startStrokeImpl(MoveSelectedLayer, 0)) { setMode(KisTool::PAINT_MODE); } // Larger movement if "shift" key is pressed. qreal scale = big ? m_optionsWidget->moveScale() : 1.0; qreal moveStep = m_optionsWidget->moveStep() * scale; QPoint offset = direction == Up ? QPoint( 0, -moveStep) : direction == Down ? QPoint( 0, moveStep) : direction == Left ? QPoint(-moveStep, 0) : QPoint( moveStep, 0) ; const bool showCoordinates = m_optionsWidget->showCoordinates(); if (showCoordinates) { KisCanvas2 *kisCanvas = dynamic_cast(canvas()); kisCanvas->viewManager()-> showFloatingMessage( i18nc("floating message in move tool", "X: %1 px, Y: %2 px", (m_startPosition + offset).x(), (m_startPosition + offset).y()), QIcon(), 1000, KisFloatingMessage::High); } KisSignalsBlocker b(m_optionsWidget); emit moveInNewPosition(m_startPosition + offset); m_startPosition += offset; image()->addJob(m_strokeId, new MoveStrokeStrategy::Data(m_accumulatedOffset + offset)); m_accumulatedOffset += offset; m_moveInProgress = false; emit moveInProgressChanged(); setMode(KisTool::HOVER_MODE); } void KisToolMove::activate(ToolActivation toolActivation, const QSet &shapes) { KisTool::activate(toolActivation, shapes); QRect totalBounds; slotNodeChanged(this->selectedNodes()); } void KisToolMove::paint(QPainter& gc, const KoViewConverter &converter) { Q_UNUSED(converter); if (m_dragInProgress) { QPainterPath handles; handles.addRect(m_handlesRect.translated(m_pos)); QPainterPath path = pixelToView(handles); paintToolOutline(&gc, path); } } void KisToolMove::initHandles(const KisNodeList &nodes) { m_handlesRect = QRect(); for (KisNodeSP node : nodes) { node->exactBounds(); m_handlesRect |= node->exactBounds(); } if (image()->globalSelection()) { m_handlesRect &= image()->globalSelection()->selectedRect(); } if (m_handlesRect.topLeft() != m_startPosition) { m_handlesRect.moveTopLeft(m_startPosition); } } void KisToolMove::deactivate() { endStroke(); KisTool::deactivate(); } void KisToolMove::requestStrokeEnd() { endStroke(); } void KisToolMove::requestStrokeCancellation() { cancelStroke(); } void KisToolMove::requestUndoDuringStroke() { - // we shouldn't cancel the stroke on Ctrl+Z, becasue it will not only + // we shouldn't cancel the stroke on Ctrl+Z, because it will not only // cancel the stroke, but also undo the previous command, which we haven't // yet pushed to the stack } void KisToolMove::beginPrimaryAction(KoPointerEvent *event) { startAction(event, moveToolMode()); } void KisToolMove::continuePrimaryAction(KoPointerEvent *event) { continueAction(event); } void KisToolMove::endPrimaryAction(KoPointerEvent *event) { endAction(event); } void KisToolMove::beginAlternateAction(KoPointerEvent *event, AlternateAction action) { // Ctrl+Right click toggles between moving current layer and moving layer w/ content if (action == PickFgNode || action == PickBgImage) { MoveToolMode mode = moveToolMode(); if (mode == MoveSelectedLayer) { mode = MoveFirstLayer; } else if (mode == MoveFirstLayer) { mode = MoveSelectedLayer; } startAction(event, mode); } else { startAction(event, MoveGroup); } } void KisToolMove::continueAlternateAction(KoPointerEvent *event, AlternateAction action) { Q_UNUSED(action) continueAction(event); } void KisToolMove::endAlternateAction(KoPointerEvent *event, AlternateAction action) { Q_UNUSED(action) endAction(event); } void KisToolMove::startAction(KoPointerEvent *event, MoveToolMode mode) { QPoint pos = convertToPixelCoordAndSnap(event).toPoint(); m_dragStart = pos; m_pos = QPoint(); m_moveInProgress = true; m_dragInProgress = true; emit moveInProgressChanged(); if (startStrokeImpl(mode, &pos)) { setMode(KisTool::PAINT_MODE); } else { event->ignore(); } m_canvas->updateCanvas(); } void KisToolMove::continueAction(KoPointerEvent *event) { CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE); if (!m_strokeId) return; QPoint pos = convertToPixelCoordAndSnap(event).toPoint(); const bool showCoordinates = m_optionsWidget ? m_optionsWidget->showCoordinates() : true; if (showCoordinates) { KisCanvas2 *kisCanvas = dynamic_cast(canvas()); kisCanvas->viewManager()-> showFloatingMessage( i18nc("floating message in move tool", "X: %1 px, Y: %2 px", (m_startPosition + (pos - m_dragStart)).x(), (m_startPosition + (pos - m_dragStart)).y()), QIcon(), 1000, KisFloatingMessage::High); } KisSignalsBlocker b(m_optionsWidget); emit moveInNewPosition(m_startPosition + (pos - m_dragStart)); pos = applyModifiers(event->modifiers(), pos); drag(pos); m_pos = pos - m_dragStart; m_canvas->updateCanvas(); } void KisToolMove::endAction(KoPointerEvent *event) { CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE); setMode(KisTool::HOVER_MODE); if (!m_strokeId) return; QPoint pos = convertToPixelCoordAndSnap(event).toPoint(); pos = applyModifiers(event->modifiers(), pos); drag(pos); m_pos = pos - m_dragStart; m_dragInProgress = false; m_startPosition += pos - m_dragStart; m_accumulatedOffset += pos - m_dragStart; m_canvas->updateCanvas(); } void KisToolMove::drag(const QPoint& newPos) { KisImageWSP image = currentImage(); QPoint offset = m_accumulatedOffset + newPos - m_dragStart; image->addJob(m_strokeId, new MoveStrokeStrategy::Data(offset)); } void KisToolMove::endStroke() { if (!m_strokeId) return; KisImageSP image = currentImage(); image->endStroke(m_strokeId); m_strokeId.clear(); m_currentlyProcessingNodes.clear(); m_moveInProgress = false; emit moveInProgressChanged(); } void KisToolMove::cancelStroke() { if (!m_strokeId) return; KisImageSP image = currentImage(); image->cancelStroke(m_strokeId); m_strokeId.clear(); m_currentlyProcessingNodes.clear(); m_moveInProgress = false; emit moveInProgressChanged(); // we should reset m_startPosition into the original value when // the stroke is cancelled KisCanvas2 *canvas = dynamic_cast(this->canvas()); canvas->viewManager()->blockUntilOperationsFinishedForced(image); slotNodeChanged(this->selectedNodes()); } QWidget* KisToolMove::createOptionWidget() { if (!currentImage()) return 0; m_optionsWidget = new MoveToolOptionsWidget(0, currentImage()->xRes(), toolId()); // See https://bugs.kde.org/show_bug.cgi?id=316896 QWidget *specialSpacer = new QWidget(m_optionsWidget); specialSpacer->setObjectName("SpecialSpacer"); specialSpacer->setFixedSize(0, 0); m_optionsWidget->layout()->addWidget(specialSpacer); m_optionsWidget->setFixedHeight(m_optionsWidget->sizeHint().height()); connect(m_showCoordinatesAction, SIGNAL(triggered(bool)), m_optionsWidget, SLOT(setShowCoordinates(bool))); connect(m_optionsWidget, SIGNAL(showCoordinatesChanged(bool)), m_showCoordinatesAction, SLOT(setChecked(bool))); m_showCoordinatesAction->setChecked(m_optionsWidget->showCoordinates()); m_optionsWidget->slotSetTranslate(m_startPosition); connect(m_optionsWidget, SIGNAL(sigSetTranslateX(int)), SLOT(moveBySpinX(int))); connect(m_optionsWidget, SIGNAL(sigSetTranslateY(int)), SLOT(moveBySpinY(int))); connect(this, SIGNAL(moveInNewPosition(QPoint)), m_optionsWidget, SLOT(slotSetTranslate(QPoint))); KisCanvas2 *kisCanvas = dynamic_cast(canvas()); connect(kisCanvas->viewManager()->nodeManager(), SIGNAL(sigUiNeedChangeSelectedNodes(KisNodeList)), this, SLOT(slotNodeChanged(KisNodeList))); return m_optionsWidget; } KisToolMove::MoveToolMode KisToolMove::moveToolMode() const { if (m_optionsWidget) return m_optionsWidget->mode(); return MoveSelectedLayer; } bool KisToolMove::moveInProgress() const { return m_moveInProgress; } QPoint KisToolMove::applyModifiers(Qt::KeyboardModifiers modifiers, QPoint pos) { QPoint move = pos - m_dragStart; // Snap to axis if (modifiers & Qt::ShiftModifier) { move = snapToClosestAxis(move); } // "Precision mode" - scale down movement by 1/5 if (modifiers & Qt::AltModifier) { const qreal SCALE_FACTOR = .2; move = SCALE_FACTOR * move; } return m_dragStart + move; } void KisToolMove::moveBySpinX(int newX) { if (mode() == KisTool::PAINT_MODE) return; // Don't interact with dragging if (!currentNode()->isEditable()) return; // Don't move invisible nodes if (startStrokeImpl(MoveSelectedLayer, 0)) { setMode(KisTool::PAINT_MODE); } int offsetX = newX - m_startPosition.x(); QPoint offset = QPoint(offsetX, 0); KisSignalsBlocker b(m_optionsWidget); emit moveInNewPosition(m_startPosition + offset); image()->addJob(m_strokeId, new MoveStrokeStrategy::Data(m_accumulatedOffset + offset)); m_accumulatedOffset += offset; m_startPosition += offset; m_moveInProgress = false; emit moveInProgressChanged(); setMode(KisTool::HOVER_MODE); } void KisToolMove::moveBySpinY(int newY) { if (mode() == KisTool::PAINT_MODE) return; // Don't interact with dragging if (!currentNode()->isEditable()) return; // Don't move invisible nodes if (startStrokeImpl(MoveSelectedLayer, 0)) { setMode(KisTool::PAINT_MODE); } int offsetY = newY - m_startPosition.y(); QPoint offset = QPoint(0, offsetY); KisSignalsBlocker b(m_optionsWidget); emit moveInNewPosition(m_startPosition + offset); image()->addJob(m_strokeId, new MoveStrokeStrategy::Data(m_accumulatedOffset + offset)); m_accumulatedOffset += offset; m_startPosition += offset; m_moveInProgress = false; emit moveInProgressChanged(); setMode(KisTool::HOVER_MODE); } void KisToolMove::slotNodeChanged(KisNodeList nodes) { QRect totalBounds; Q_FOREACH (KisNodeSP node, nodes) { if (node && node->projection()) { totalBounds |= node->projection()->nonDefaultPixelArea(); } } if (image()->globalSelection()) { totalBounds &= image()->globalSelection()->selectedRect(); } m_startPosition = totalBounds.topLeft(); if (m_optionsWidget) { KisSignalsBlocker b(m_optionsWidget); m_optionsWidget->slotSetTranslate(m_startPosition); } } diff --git a/plugins/tools/karbonplugins/tools/CalligraphyTool/BUGS b/plugins/tools/karbonplugins/tools/CalligraphyTool/BUGS index 0fdb0a1081..73fa1eb3c5 100644 --- a/plugins/tools/karbonplugins/tools/CalligraphyTool/BUGS +++ b/plugins/tools/karbonplugins/tools/CalligraphyTool/BUGS @@ -1,12 +1,12 @@ BUGS: * freehand drawing tool with curve: sometimes the result disappears (seems to be caused by bezierFit receiving two points in sequence that are the same) * when painting something at the side it becomes impossible to scroll to all areas of the screen * when painting while the view is zoomed in very much in, the item may get somewere outside the visible area. * ctrl+mouse weel zoom don't center correctly * at times the cpu usage remains very high even without doing anything (shape proprieties seem to be the cause) * colors used are not those of the global palette (I think) -* a "degenerate" curve doesn't show anything... I mean when you have only two points in the same position -- if it has control points it shows as expected in inkscape, firefox, opera, and openoffice, not in konqueror. When it hasn't control points I whould expect it to show, nothing, a circle and a square respectively when using But Cap, Round Cap, and Square Cap. I only tested this for inkscape, which showed this behavior except in the third case, where it didn't show anything. +* a "degenerate" curve doesn't show anything... I mean when you have only two points in the same position -- if it has control points it shows as expected in inkscape, firefox, opera, and openoffice, not in konqueror. When it hasn't control points I would expect it to show, nothing, a circle and a square respectively when using But Cap, Round Cap, and Square Cap. I only tested this for inkscape, which showed this behavior except in the third case, where it didn't show anything. * In a path without control points sometimes at one side there is still a piece of path when there shouldn't... (maybe a Qt bug if it uses QPainterPath to paint) * in KoParameterShape there is a repainting error when moving the control points * when the path generated by the calligraphy tool has a _lot_ of points bezierFit() crashes * KoPathPoint.cpp:374 Does fuzzycompare with 0.0 work, I don't think so.. \ No newline at end of file diff --git a/plugins/tools/tool_smart_patch/kis_tool_smart_patch.cpp b/plugins/tools/tool_smart_patch/kis_tool_smart_patch.cpp index 8aae695636..656ba388ca 100644 --- a/plugins/tools/tool_smart_patch/kis_tool_smart_patch.cpp +++ b/plugins/tools/tool_smart_patch/kis_tool_smart_patch.cpp @@ -1,271 +1,271 @@ /* * Copyright (c) 2017 Eugene Ingerman * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_tool_smart_patch.h" #include "QApplication" #include "QPainterPath" #include #include #include #include "kis_canvas2.h" #include "kis_cursor.h" #include "kis_painter.h" #include "kis_paintop_preset.h" #include "kundo2magicstring.h" #include "kundo2stack.h" #include "kis_transaction_based_command.h" #include "kis_transaction.h" #include "kis_processing_applicator.h" #include "kis_datamanager.h" #include "KoColorSpaceRegistry.h" #include "kis_tool_smart_patch_options_widget.h" #include "libs/image/kis_paint_device_debug_utils.h" #include "kis_paint_layer.h" QRect patchImage(KisPaintDeviceSP imageDev, KisPaintDeviceSP maskDev, int radius, int accuracy); class KisToolSmartPatch::InpaintCommand : public KisTransactionBasedCommand { public: InpaintCommand( KisPaintDeviceSP maskDev, KisPaintDeviceSP imageDev, int accuracy, int patchRadius ) : m_maskDev(maskDev), m_imageDev(imageDev), m_accuracy(accuracy), m_patchRadius(patchRadius) {} KUndo2Command* paint() override { KisTransaction transaction(m_imageDev); patchImage(m_imageDev, m_maskDev, m_patchRadius, m_accuracy); return transaction.endAndTake(); } private: KisPaintDeviceSP m_maskDev, m_imageDev; int m_accuracy, m_patchRadius; }; struct KisToolSmartPatch::Private { KisPaintDeviceSP maskDev = nullptr; KisPainter maskDevPainter; float brushRadius = 50.; //initial default. actually read from ui. KisToolSmartPatchOptionsWidget *optionsWidget = nullptr; QRectF oldOutlineRect; QPainterPath brushOutline; }; KisToolSmartPatch::KisToolSmartPatch(KoCanvasBase * canvas) : KisToolPaint(canvas, KisCursor::blankCursor()), m_d(new Private) { setSupportOutline(true); setObjectName("tool_SmartPatch"); m_d->maskDev = new KisPaintDevice(KoColorSpaceRegistry::instance()->rgb8()); m_d->maskDevPainter.begin( m_d->maskDev ); m_d->maskDevPainter.setPaintColor(KoColor(Qt::magenta, m_d->maskDev->colorSpace())); m_d->maskDevPainter.setBackgroundColor(KoColor(Qt::white, m_d->maskDev->colorSpace())); m_d->maskDevPainter.setFillStyle( KisPainter::FillStyleForegroundColor ); } KisToolSmartPatch::~KisToolSmartPatch() { m_d->optionsWidget = nullptr; m_d->maskDevPainter.end(); } void KisToolSmartPatch::activate(ToolActivation activation, const QSet &shapes) { KisToolPaint::activate(activation, shapes); } void KisToolSmartPatch::deactivate() { KisToolPaint::deactivate(); } void KisToolSmartPatch::resetCursorStyle() { KisToolPaint::resetCursorStyle(); } void KisToolSmartPatch::activatePrimaryAction() { setOutlineEnabled(true); KisToolPaint::activatePrimaryAction(); } void KisToolSmartPatch::deactivatePrimaryAction() { setOutlineEnabled(false); KisToolPaint::deactivatePrimaryAction(); } void KisToolSmartPatch::addMaskPath( KoPointerEvent *event ) { QPointF imagePos = currentImage()->documentToPixel(event->point); QPainterPath currentBrushOutline = brushOutline().translated(imagePos); m_d->maskDevPainter.fillPainterPath(currentBrushOutline); canvas()->updateCanvas(currentImage()->pixelToDocument(m_d->maskDev->exactBounds())); } void KisToolSmartPatch::beginPrimaryAction(KoPointerEvent *event) { //we can only apply inpaint operation to paint layer if ( currentNode().isNull() || !currentNode()->inherits("KisPaintLayer") || nodePaintAbility()!=NodePaintAbility::PAINT ) { KisCanvas2 * kiscanvas = static_cast(canvas()); kiscanvas->viewManager()-> showFloatingMessage( i18n("Select a paint layer to use this tool"), QIcon(), 2000, KisFloatingMessage::Medium, Qt::AlignCenter); event->ignore(); return; } addMaskPath(event); setMode(KisTool::PAINT_MODE); KisToolPaint::beginPrimaryAction(event); } void KisToolSmartPatch::continuePrimaryAction(KoPointerEvent *event) { CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE); addMaskPath(event); KisToolPaint::continuePrimaryAction(event); } void KisToolSmartPatch::endPrimaryAction(KoPointerEvent *event) { CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE); addMaskPath(event); KisToolPaint::endPrimaryAction(event); setMode(KisTool::HOVER_MODE); QApplication::setOverrideCursor(KisCursor::waitCursor()); int accuracy = 50; //default accuracy - middle value int patchRadius = 4; //default radius, which works well for most cases tested if (m_d->optionsWidget) { accuracy = m_d->optionsWidget->getAccuracy(); patchRadius = m_d->optionsWidget->getPatchRadius(); } KisProcessingApplicator applicator( image(), currentNode(), KisProcessingApplicator::NONE, KisImageSignalVector() << ModifiedSignal, kundo2_i18n("Smart Patch")); //actual inpaint operation. filling in areas masked by user applicator.applyCommand( new InpaintCommand( KisPainter::convertToAlphaAsAlpha(m_d->maskDev), currentNode()->paintDevice(), accuracy, patchRadius ), KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE ); applicator.end(); image()->waitForDone(); QApplication::restoreOverrideCursor(); m_d->maskDev->clear(); } QPainterPath KisToolSmartPatch::brushOutline( void ) { const qreal diameter = m_d->brushRadius; QPainterPath outline; outline.addEllipse(QPointF(0,0), -0.5 * diameter, -0.5 * diameter ); return outline; } QPainterPath KisToolSmartPatch::getBrushOutlinePath(const QPointF &documentPos, const KoPointerEvent *event) { Q_UNUSED(event); QPointF imagePos = currentImage()->documentToPixel(documentPos); QPainterPath path = brushOutline(); return path.translated( imagePos.rx(), imagePos.ry() ); } void KisToolSmartPatch::requestUpdateOutline(const QPointF &outlineDocPoint, const KoPointerEvent *event) { static QPointF lastDocPoint = QPointF(0,0); if( event ) lastDocPoint=outlineDocPoint; m_d->brushRadius = currentPaintOpPreset()->settings()->paintOpSize(); m_d->brushOutline = getBrushOutlinePath(lastDocPoint, event); QRectF outlinePixelRect = m_d->brushOutline.boundingRect(); QRectF outlineDocRect = currentImage()->pixelToDocument(outlinePixelRect); // This adjusted call is needed as we paint with a 3 pixel wide brush and the pen is outside the bounds of the path - // Pen uses view coordinates so we have to zoom the document value to match 2 pixel in view coordiates + // Pen uses view coordinates so we have to zoom the document value to match 2 pixel in view coordinates // See BUG 275829 qreal zoomX; qreal zoomY; canvas()->viewConverter()->zoom(&zoomX, &zoomY); qreal xoffset = 2.0/zoomX; qreal yoffset = 2.0/zoomY; if (!outlineDocRect.isEmpty()) { outlineDocRect.adjust(-xoffset,-yoffset,xoffset,yoffset); } if (!m_d->oldOutlineRect.isEmpty()) { canvas()->updateCanvas(m_d->oldOutlineRect); } if (!outlineDocRect.isEmpty()) { canvas()->updateCanvas(outlineDocRect); } m_d->oldOutlineRect = outlineDocRect; } void KisToolSmartPatch::paint(QPainter &painter, const KoViewConverter &converter) { Q_UNUSED(converter); painter.save(); painter.setCompositionMode(QPainter::RasterOp_SourceXorDestination); painter.setPen(QColor(128, 255, 128)); painter.drawPath(pixelToView(m_d->brushOutline)); painter.restore(); painter.save(); painter.setBrush(Qt::magenta); QImage img = m_d->maskDev->convertToQImage(0); if( !img.size().isEmpty() ){ painter.drawImage(pixelToView(m_d->maskDev->exactBounds()), img); } painter.restore(); } QWidget * KisToolSmartPatch::createOptionWidget() { KisCanvas2 * kiscanvas = dynamic_cast(canvas()); m_d->optionsWidget = new KisToolSmartPatchOptionsWidget(kiscanvas->viewManager()->resourceProvider(), 0); m_d->optionsWidget->setObjectName(toolId() + "option widget"); return m_d->optionsWidget; } diff --git a/sdk/tests/KisRectsCollisionsTracker.h b/sdk/tests/KisRectsCollisionsTracker.h index b2af80fde3..bca9d7d400 100644 --- a/sdk/tests/KisRectsCollisionsTracker.h +++ b/sdk/tests/KisRectsCollisionsTracker.h @@ -1,67 +1,67 @@ /* * Copyright (c) 2017 Dmitry Kazakov * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KISRECTSCOLLISIONSTRACKER_H #define KISRECTSCOLLISIONSTRACKER_H #include #include #include #include #include "kis_assert.h" class KisRectsCollisionsTracker { public: void startAccessingRect(const QRect &rc) { QMutexLocker l(&m_mutex); checkUniqueAccessImpl(rc, "start"); m_rectsInProgress.append(rc); } void endAccessingRect(const QRect &rc) { QMutexLocker l(&m_mutex); const bool result = m_rectsInProgress.removeOne(rc); KIS_SAFE_ASSERT_RECOVER_NOOP(result); checkUniqueAccessImpl(rc, "end"); } private: bool checkUniqueAccessImpl(const QRect &rect, const QString &tag) { Q_FOREACH (const QRect &rc, m_rectsInProgress) { if (rc != rect && rect.intersects(rc)) { - ENTER_FUNCTION() << "FAIL: concurrect access from" << rect << "to" << rc << tag; + ENTER_FUNCTION() << "FAIL: concurrent access from" << rect << "to" << rc << tag; return false; } } return true; } private: QList m_rectsInProgress; QMutex m_mutex; }; #endif // KISRECTSCOLLISIONSTRACKER_H