diff --git a/HACKING b/HACKING index 566e11e9ae..25b3040b47 100644 --- a/HACKING +++ b/HACKING @@ -1,165 +1,165 @@ Since 1999, people have been hacking on Krita. Everyone brought their own coding style, their own code conventions, their own likes and dislikes. Me, (Boudewijn that is), I like indents of four spaces, and no scope prefixes for variables. However, in the interests of consistency, these are the rules new code should adhere to: See also http://techbase.kde.org/Policies/Kdelibs_Coding_Style -- that document is leading. Qt vs STD vs Boost: In general, use the Qt classes wherever possible, even if someone tells you that the STD class is better or whatever. We're dealing with a big project with lots of developers here and we should keep the code as consistent and easy to grasp as possible. Since people need to know Qt in the first place, keep to Qt. Discuss deviations on #krita C++11 and C++14 Yes, but. Avoid lambdas (except when replacing the use of QSignalMapper. Sigh.). Avoid the new sig/slot connection syntax _unless_ you are porting all of Krita to the new syntax. Sure, it has some advantages, but having two different ways of doing the same thing is begging for trouble and comprehension problems. For now, keep using Q_FOREACH, we're using it all over the place. auto is fine, when using in for loops. Don't go use it in other places. Before using other new features, discuss on #krita so we can expand this list. Our minimum gcc version is 4.5, shipped with Ubuntu 12.04 Indentation With four spaces. Use the default kdelibs indentation (http://techbase.kde.org/Policies/Kdelibs_Coding_Style) Includes Avoid as much as possible #includes in header files; use forward declarations of classes. Initializers Avoid as much as possible initializers in the body of the constructor. Use initializer lists instead. Write the initializers as follows Class(A a, B b) : Subclass(a) , m_b(b) { } Note the location of the colon and comma. It is also okay to use initializers like, and maybe even preferred where it works. int m_something {0}; Scope prefixes Use only m_ for class-level variables. No other scope prefixes; no g_, l_, no 'p' for pointer variables. Shared pointers Use shared pointers wherever possible. Prefer Qt's shared pointer classes to our home-grown shared ppointer classes. Getter/setter Krita doesn't use Qt's properties -- yet. If you want to introduce use of properties, convert any and all classes in Krita before committing. Getter/setters are named 'x() for getters and setX(int x) for setters. If you come across violations of this rule, change the code. Class naming If you use a well-known design pattern, name the class according to the design pattern. All files should start with 'Kis', all classes with the 'Kis' prefix. This filename should be the same as the classname: KisNewClass.h, KisNewClass. Function naming Functions should be named in camelBackedFashion, to conform to Qt's standards. If you encounter functions in c_style_like_this, feel free to rename. Also: verbNoun -- i.e., rotateLayer, not layer_rotate. The latter is a true c-ism, introduced by a language that needs to prefix the 'class' name to every function in order to have something that's not quite OO. Variable/Parameter names Variable/parameter names start with a lower case letter. A name composed of different words is done in camelBackedStyle. Designer Krita has started to use designer. All dialogs and all widgets that have a layout manager must be done in designer. Do not add code or signal/slot connections in designer. Enums All enums should be prefixed with 'enum'. Namespaces Currently, we only use anonymous namespaces for things like undo commands. For the rest, some classes have a 'Kis' prefix, others don't. This should be made consistent, and we might want to use namespaces to keep all of Krita inside. Files and classes It's preferred (and strongly preferred) to have only one class per .h/.cpp file. (Which is logical, because otherwise you won't be able to keep to the naming scheme.) Spaces Keep the source airy and open. In particular, there should be empty lines between function declarations and definitions. Slots and signals Prefix slots with slot and signals with sig: slotUpdateSelection, sigSelectionUpdated. Boolean operators Use the standard !, !=, ==, && etc style, not the "not", "and" etc. style. Keep krita code using one, easily recognizable, C++ style. Boudewijn Rempt With Krita now supporting Python scripting, we need guidelines for these as well. These guidelines are preliminary and may be further refined in the future. To keep it simple, we have chosen to follow the style guide suggested by Python: PEP8. All rules should be followed, except the max limit of 79 characters per line. As this can reduce readability in some cases, this rule is optional. The full PEP8 specification is available here: https://www.python.org/dev/peps/pep-0008/ To check compliance you can run pep8.py against the code. You can also use autopep8.py to automatically fix detected compliance issues. pep8.py can be downloaded via Python's package manager (pip) [https://pypi.python.org/pypi/pep8], or your distribution's package manager. autopep8.py can also be downloaded via Python's package manager [https://pypi.python.org/pypi/autopep8], or your distribution's package manager. Both of these scripts come bundled with the PyDev plugin, which is available for Eclipse and other IDEs. The PyDev integration can be configured to visually highlight portions of the code which is not in compliance, as well as run autopep8 via shortcuts. pep8.py and autopep8.py can suppress select rules via the "--ignore" command line argument. -To ignore the 79 charachers per line rule, pep8.py can be called like this: +To ignore the 79 characters per line rule, pep8.py can be called like this: pep8.py --ignore=E501 You can read more about the error codes and what they mean here: http://pep8.readthedocs.io/en/release-1.7.x/intro.html#error-codes diff --git a/README.android.md b/README.android.md index 3c557fdc09..9c53507a99 100644 --- a/README.android.md +++ b/README.android.md @@ -1,58 +1,58 @@ # Building Krita for Android First of all, I use linux to do my builds and testing. Although, -they _should_ work on Windows/macOS, I cannott give any guarentee +they _should_ work on Windows/macOS, I cannot give any guarantee that it will. ## Setting up Android SDK and NDK -We right now use android ndk version `r18b` to do our builds. So, +We right now use Android NDK version `r18b` to do our builds. So, I would recommend to use that. Download it from [google's website](https://developer.android.com/ndk/downloads/older_releases.html) then extract it. Next, Android SDK. You can either download Android Studio or just the `sdk-tools`. Both could be downloaded from [google's website](https://developer.android.com/studio). -If you downloaded Android Studio then open sdk manager and download +If you downloaded Android Studio then open SDK manager and download `Android SDK Build-Tools`. (more info: https://developer.android.com/studio/intro/update#sdk-manager) If you download just `sdk-tools`, then, extract it and run: ```shell cd /tools/bin ./sdkmanager --licenses ./sdkmanager platform-tools ./sdkmanager "platforms;android-21" ./sdkmanager "platforms;android-28" # for androiddeployqt ./sdkmanager "build-tools;28.0.2" ``` If you get some `ClasNotFoundException` it might be because `java` version is set to `11`. For `sdkmanager` to work, set it to `8` and then run it again. That's the only dependency we have to manage manually! ## Building Krita Now, to build krita, run `/packaging/android/androidbuild.sh --help` and pass the required arguments. Example: ```shell ./androidbuild.sh -p=all --src=/home/sh_zam/workspace/krita --build-type=Debug --build-root=/home/sh_zam/workspace/build-krita-android --ndk-path=/home/sh_zam/Android/Sdk/ndk-bundle --sdk-path=/home/sh_zam/Android/Sdk --api-level=21 --android-abi=armeabi-v7a ``` That's all! ## Installing Krita APK To install run `adb install -d -r /krita_build_apk/build/outputs/apk/debug/krita_build_apk-debug.apk`. `adb` should be in `/platform-tools/` ## Crash If Krita crashes you can look up the logs using `adb logcat` diff --git a/krita/doc/strokes/strokes_documentation.org b/krita/doc/strokes/strokes_documentation.org index 2c3a5917a3..a2141f906e 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 - create a =KoUpdater= object for you. And all is done. You can see - an example in =KisTransformProcessingVisitor= class. + proxy of the node we process (=KisNodeProgressProxy=). Our + processings framework provides an even easier way of doing this. + You just need to instantiate a =ProgressHelper= object and ask it + to 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/brush/kis_auto_brush.cpp b/libs/brush/kis_auto_brush.cpp index 72c445afa1..7fa8804edf 100644 --- a/libs/brush/kis_auto_brush.cpp +++ b/libs/brush/kis_auto_brush.cpp @@ -1,398 +1,398 @@ /* * Copyright (c) 2004,2007-2009 Cyrille Berger * Copyright (c) 2010 Lukáš Tvrdý * Copyright (c) 2012 Sven Langkamp * * 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 //MSVC requires that Vc come first #include "kis_auto_brush.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(_WIN32) || defined(_WIN64) #include #define srand48 srand inline double drand48() { return double(rand()) / RAND_MAX; } #endif struct KisAutoBrush::Private { Private() : randomness(0), density(1.0), idealThreadCountCached(1) {} Private(const Private &rhs) : shape(rhs.shape->clone()), randomness(rhs.randomness), density(rhs.density), idealThreadCountCached(rhs.idealThreadCountCached) { } QScopedPointer shape; qreal randomness; qreal density; int idealThreadCountCached; }; KisAutoBrush::KisAutoBrush(KisMaskGenerator* as, qreal angle, qreal randomness, qreal density) : KisBrush(), d(new Private) { d->shape.reset(as); d->randomness = randomness; d->density = density; d->idealThreadCountCached = QThread::idealThreadCount(); setBrushType(MASK); setWidth(qMax(qreal(1.0), d->shape->width())); setHeight(qMax(qreal(1.0), d->shape->height())); QImage image = createBrushPreview(); setBrushTipImage(image); // Set angle here so brush tip image is generated unrotated setAngle(angle); image = createBrushPreview(); setImage(image); } KisAutoBrush::~KisAutoBrush() { } qreal KisAutoBrush::userEffectiveSize() const { return d->shape->diameter(); } void KisAutoBrush::setUserEffectiveSize(qreal value) { d->shape->setDiameter(value); } KisAutoBrush::KisAutoBrush(const KisAutoBrush& rhs) : KisBrush(rhs), d(new Private(*rhs.d)) { } KisBrush* KisAutoBrush::clone() const { return new KisAutoBrush(*this); } -/* It's difficult to predict the mask height when exaclty when there are +/* It's difficult to predict the mask height when exactly when there are * more than 2 spikes, so we return an upperbound instead. */ static KisDabShape lieAboutDabShape(KisDabShape const& shape) { return KisDabShape(shape.scale(), 1.0, shape.rotation()); } qint32 KisAutoBrush::maskHeight(KisDabShape const& shape, qreal subPixelX, qreal subPixelY, const KisPaintInformation& info) const { return KisBrush::maskHeight( lieAboutDabShape(shape), subPixelX, subPixelY, info); } qint32 KisAutoBrush::maskWidth(KisDabShape const& shape, qreal subPixelX, qreal subPixelY, const KisPaintInformation& info) const { return KisBrush::maskWidth( lieAboutDabShape(shape), subPixelX, subPixelY, info); } QSizeF KisAutoBrush::characteristicSize(KisDabShape const& shape) const { return KisBrush::characteristicSize(lieAboutDabShape(shape)); } inline void fillPixelOptimized_4bytes(quint8 *color, quint8 *buf, int size) { /** * This version of filling uses low granularity of data transfers * (32-bit chunks) and internal processor's parallelism. It reaches * 25% better performance in KisStrokeBenchmark in comparison to * per-pixel memcpy version (tested on Sandy Bridge). */ int block1 = size / 8; int block2 = size % 8; quint32 *src = reinterpret_cast(color); quint32 *dst = reinterpret_cast(buf); // check whether all buffers are 4 bytes aligned // (uncomment if experience some problems) // Q_ASSERT(((qint64)src & 3) == 0); // Q_ASSERT(((qint64)dst & 3) == 0); for (int i = 0; i < block1; i++) { *dst = *src; *(dst + 1) = *src; *(dst + 2) = *src; *(dst + 3) = *src; *(dst + 4) = *src; *(dst + 5) = *src; *(dst + 6) = *src; *(dst + 7) = *src; dst += 8; } for (int i = 0; i < block2; i++) { *dst = *src; dst++; } } inline void fillPixelOptimized_general(quint8 *color, quint8 *buf, int size, int pixelSize) { /** * This version uses internal processor's parallelism and gives * 20% better performance in KisStrokeBenchmark in comparison to * per-pixel memcpy version (tested on Sandy Bridge (+20%) and * on Merom (+10%)). */ int block1 = size / 8; int block2 = size % 8; for (int i = 0; i < block1; i++) { quint8 *d1 = buf; quint8 *d2 = buf + pixelSize; quint8 *d3 = buf + 2 * pixelSize; quint8 *d4 = buf + 3 * pixelSize; quint8 *d5 = buf + 4 * pixelSize; quint8 *d6 = buf + 5 * pixelSize; quint8 *d7 = buf + 6 * pixelSize; quint8 *d8 = buf + 7 * pixelSize; for (int j = 0; j < pixelSize; j++) { *(d1 + j) = color[j]; *(d2 + j) = color[j]; *(d3 + j) = color[j]; *(d4 + j) = color[j]; *(d5 + j) = color[j]; *(d6 + j) = color[j]; *(d7 + j) = color[j]; *(d8 + j) = color[j]; } buf += 8 * pixelSize; } for (int i = 0; i < block2; i++) { memcpy(buf, color, pixelSize); buf += pixelSize; } } void KisAutoBrush::generateMaskAndApplyMaskOrCreateDab(KisFixedPaintDeviceSP dst, KisBrush::ColoringInformation* coloringInformation, KisDabShape const& shape, const KisPaintInformation& info, double subPixelX , double subPixelY, qreal softnessFactor) const { Q_UNUSED(info); // Generate the paint device from the mask const KoColorSpace* cs = dst->colorSpace(); quint32 pixelSize = cs->pixelSize(); // mask dimension methods already includes KisBrush::angle() int dstWidth = maskWidth(shape, subPixelX, subPixelY, info); int dstHeight = maskHeight(shape, subPixelX, subPixelY, info); QPointF hotSpot = this->hotSpot(shape, info); // mask size and hotSpot function take the KisBrush rotation into account qreal angle = shape.rotation() + KisBrush::angle(); // if there's coloring information, we merely change the alpha: in that case, // the dab should be big enough! if (coloringInformation) { // new bounds. we don't care if there is some extra memory occcupied. dst->setRect(QRect(0, 0, dstWidth, dstHeight)); dst->lazyGrowBufferWithoutInitialization(); } else { KIS_SAFE_ASSERT_RECOVER_RETURN(dst->bounds().width() >= dstWidth && dst->bounds().height() >= dstHeight); } quint8* dabPointer = dst->data(); quint8* color = 0; if (coloringInformation) { if (dynamic_cast(coloringInformation)) { color = const_cast(coloringInformation->color()); } } double centerX = hotSpot.x() - 0.5 + subPixelX; double centerY = hotSpot.y() - 0.5 + subPixelY; d->shape->setSoftness(softnessFactor); // softness must be set first d->shape->setScale(shape.scaleX(), shape.scaleY()); if (coloringInformation) { if (color && pixelSize == 4) { fillPixelOptimized_4bytes(color, dabPointer, dstWidth * dstHeight); } else if (color) { fillPixelOptimized_general(color, dabPointer, dstWidth * dstHeight, pixelSize); } else { for (int y = 0; y < dstHeight; y++) { for (int x = 0; x < dstWidth; x++) { memcpy(dabPointer, coloringInformation->color(), pixelSize); coloringInformation->nextColumn(); dabPointer += pixelSize; } coloringInformation->nextRow(); } } } MaskProcessingData data(dst, cs, d->randomness, d->density, centerX, centerY, angle); KisBrushMaskApplicatorBase *applicator = d->shape->applicator(); applicator->initializeData(&data); int jobs = d->idealThreadCountCached; if (threadingAllowed() && dstHeight > 100 && jobs >= 4) { int splitter = dstHeight / jobs; QVector rects; for (int i = 0; i < jobs - 1; i++) { rects << QRect(0, i * splitter, dstWidth, splitter); } rects << QRect(0, (jobs - 1)*splitter, dstWidth, dstHeight - (jobs - 1)*splitter); OperatorWrapper wrapper(applicator); QtConcurrent::blockingMap(rects, wrapper); } else { QRect rect(0, 0, dstWidth, dstHeight); applicator->process(rect); } } void KisAutoBrush::toXML(QDomDocument& doc, QDomElement& e) const { QDomElement shapeElt = doc.createElement("MaskGenerator"); d->shape->toXML(doc, shapeElt); e.appendChild(shapeElt); e.setAttribute("type", "auto_brush"); e.setAttribute("spacing", QString::number(spacing())); e.setAttribute("useAutoSpacing", QString::number(autoSpacingActive())); e.setAttribute("autoSpacingCoeff", QString::number(autoSpacingCoeff())); e.setAttribute("angle", QString::number(KisBrush::angle())); e.setAttribute("randomness", QString::number(d->randomness)); e.setAttribute("density", QString::number(d->density)); KisBrush::toXML(doc, e); } QImage KisAutoBrush::createBrushPreview() { int width = maskWidth(KisDabShape(), 0.0, 0.0, KisPaintInformation()); int height = maskHeight(KisDabShape(), 0.0, 0.0, KisPaintInformation()); KisPaintInformation info(QPointF(width * 0.5, height * 0.5), 0.5, 0, 0, angle(), 0, 0, 0, 0); KisFixedPaintDeviceSP fdev = new KisFixedPaintDevice(KoColorSpaceRegistry::instance()->rgb8()); fdev->setRect(QRect(0, 0, width, height)); fdev->initialize(); mask(fdev, KoColor(Qt::black, fdev->colorSpace()), KisDabShape(), info); return fdev->convertToQImage(0); } const KisMaskGenerator* KisAutoBrush::maskGenerator() const { return d->shape.data(); } qreal KisAutoBrush::density() const { return d->density; } qreal KisAutoBrush::randomness() const { return d->randomness; } QPainterPath KisAutoBrush::outline() const { bool simpleOutline = (d->density < 1.0); if (simpleOutline) { QPainterPath path; QRectF brushBoundingbox(0, 0, width(), height()); if (maskGenerator()->type() == KisMaskGenerator::CIRCLE) { path.addEllipse(brushBoundingbox); } else { // if (maskGenerator()->type() == KisMaskGenerator::RECTANGLE) path.addRect(brushBoundingbox); } return path; } return KisBrush::boundary()->path(); } void KisAutoBrush::lodLimitations(KisPaintopLodLimitations *l) const { KisBrush::lodLimitations(l); if (!qFuzzyCompare(density(), 1.0)) { l->limitations << KoID("auto-brush-density", i18nc("PaintOp instant preview limitation", "Brush Density recommended value 100.0")); } if (!qFuzzyCompare(randomness(), 0.0)) { l->limitations << KoID("auto-brush-randomness", i18nc("PaintOp instant preview limitation", "Brush Randomness recommended value 0.0")); } } diff --git a/libs/flake/KoParameterShape.h b/libs/flake/KoParameterShape.h index 6af45a81cb..bc58824a75 100644 --- a/libs/flake/KoParameterShape.h +++ b/libs/flake/KoParameterShape.h @@ -1,163 +1,163 @@ /* This file is part of the KDE project Copyright (C) 2006 Thorsten Zachmann 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 KOPARAMETERSHAPE_H #define KOPARAMETERSHAPE_H #include "KoPathShape.h" #include "kritaflake_export.h" class KoParameterShapePrivate; class KisHandlePainterHelper; /** * KoParameterShape is the base class for all parametric shapes * in flake. * Parametric shapes are those whose appearance can be completely * defined by a few numerical parameters. Rectangle, ellipse and star * are examples of parametric shapes. * In flake, these shape parameters can be manipulated visually by means * of control points. These control points can be moved with the mouse * on the canvas which changes the shapes parameter values and hence the * shapes appearance in realtime. * KoParameterShape is derived from the KoPathShape class that means * by changing the shape parameters, the underlying path is manipulated. * A parametric shape can be converted into a path shape by simply calling * the setModified method. This makes the path tool know that it can handle * the shape like a path shape, so that modifying the single path points * is possible. */ class KRITAFLAKE_EXPORT KoParameterShape : public KoPathShape { public: KoParameterShape(); ~KoParameterShape() override; /** * @brief Move handle to point * * This method calls moveHandleAction. Overload moveHandleAction to get the behaviour you want. * After that updatePath and a repaint is called. * * @param handleId the id of the handle to move * @param point the point to move the handle to in document coordinates * @param modifiers the keyboard modifiers used during moving the handle */ void moveHandle(int handleId, const QPointF &point, Qt::KeyboardModifiers modifiers = Qt::NoModifier); /** * @brief Get the id of the handle within the given rect * * @param rect the rect in shape coordinates * @return id of the found handle or -1 if none was found */ int handleIdAt(const QRectF &rect) const; /** * @brief Get the handle position * * @param handleId the id of the handle for which to get the position in shape coordinates */ QPointF handlePosition(int handleId) const; /** * @brief Paint the handles * * @param handlesHelper the helper of the handles used for painting * @sa KisHandlePainterHelper */ void paintHandles(KisHandlePainterHelper &handlesHelper); /** * @brief Paint the given handles * * @param handlesHelper the helper of the handle used for painting * @param handleId of the handle which should be repainted */ void paintHandle(KisHandlePainterHelper &handlesHelper, int handleId); /// reimplemented from KoShape void setSize(const QSizeF &size) override; /** * @brief Check if object is a parametric shape * * It is no longer a parametric shape when the path was manipulated * - * @return true if it is a parametic shape, false otherwise + * @return true if it is a parametric shape, false otherwise */ bool isParametricShape() const; /** * @brief Set if the shape can be modified using parameters * * After the state is set to false it is no longer possible to work * with parameters on this shape. * * @param parametric the new state * @see isParametricShape */ void setParametricShape(bool parametric); QPointF normalize() override; /// return the number of handles set on the shape int handleCount() const; protected: /** * Get the handle positions for manipulating the parameters. * @see setHandles, handleCount() */ QList handles() const; /** * Set the new handle positions which are used by the user to manipulate the parameters. * @see handles(), handleCount() */ void setHandles(const QList &handles); /// constructor KoParameterShape(const KoParameterShape &rhs); /** * @brief Updates the internal state of a KoParameterShape. * * This method is called from moveHandle. * * @param handleId of the handle * @param point to move the handle to in shape coordinates * @param modifiers used during move to point */ virtual void moveHandleAction(int handleId, const QPointF & point, Qt::KeyboardModifiers modifiers = Qt::NoModifier) = 0; /** * @brief Update the path of the parameter shape * * @param size of the shape */ virtual void updatePath(const QSizeF &size) = 0; private: class Private; QSharedDataPointer d; }; #endif /* KOPARAMETERSHAPE_H */ diff --git a/libs/global/KisSignalMapper.cpp b/libs/global/KisSignalMapper.cpp index e20b8cbd6f..5de4d23f7f 100644 --- a/libs/global/KisSignalMapper.cpp +++ b/libs/global/KisSignalMapper.cpp @@ -1,304 +1,304 @@ /**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtCore module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 3 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL3 included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 3 requirements ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 2.0 or (at your option) the GNU General ** Public license version 3 or any later version approved by the KDE Free ** Qt Foundation. The licenses are as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-2.0.html and ** https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "KisSignalMapper.h" #include "qhash.h" class KisSignalMapper::Private { public: Private(KisSignalMapper *_q) :q(_q) {} void _q_senderDestroyed() { q->removeMappings(q->sender()); } QHash intHash; QHash stringHash; QHash widgetHash; QHash objectHash; KisSignalMapper *q; }; /*! \class KisSignalMapper \inmodule QtCore \obsolete The recommended solution is connecting the signal to a lambda. \brief The KisSignalMapper class bundles signals from identifiable senders. \ingroup objectmodel This class collects a set of parameterless signals, and re-emits them with integer, string or widget parameters corresponding to the object that sent the signal. The class supports the mapping of particular strings or integers with particular objects using setMapping(). The objects' signals can then be connected to the map() slot which will emit the mapped() signal with the string or integer associated with the - original signalling object. Mappings can be removed later using + original signaling object. Mappings can be removed later using removeMappings(). Example: Suppose we want to create a custom widget that contains a group of buttons (like a tool palette). One approach is to connect each button's \c clicked() signal to its own custom slot; but in this example we want to connect all the buttons to a single slot and parameterize the slot by the button that was clicked. Here's the definition of a simple custom widget that has a single signal, \c clicked(), which is emitted with the text of the button that was clicked: \snippet KisSignalMapper/buttonwidget.h 0 \snippet KisSignalMapper/buttonwidget.h 1 The only function that we need to implement is the constructor: \snippet KisSignalMapper/buttonwidget.cpp 0 \snippet KisSignalMapper/buttonwidget.cpp 1 \snippet KisSignalMapper/buttonwidget.cpp 2 A list of texts is passed to the constructor. A signal mapper is constructed and for each text in the list a QPushButton is created. We connect each button's \c clicked() signal to the signal mapper's map() slot, and create a mapping in the signal mapper from each button to the button's text. Finally we connect the signal mapper's mapped() signal to the custom widget's \c clicked() signal. When the user clicks a button, the custom widget will emit a single \c clicked() signal whose argument is the text of the button the user clicked. This class was mostly useful before lambda functions could be used as slots. The example above can be rewritten simpler without KisSignalMapper by connecting to a lambda function. \snippet KisSignalMapper/buttonwidget.cpp 3 \sa QObject, QButtonGroup, QActionGroup */ /*! Constructs a KisSignalMapper with parent \a parent. */ KisSignalMapper::KisSignalMapper(QObject* parent) : QObject(parent) , d(new Private(this)) { } /*! Destroys the KisSignalMapper. */ KisSignalMapper::~KisSignalMapper() { } /*! Adds a mapping so that when map() is signalled from the given \a sender, the signal mapped(\a id) is emitted. There may be at most one integer ID for each sender. \sa mapping() */ void KisSignalMapper::setMapping(QObject *sender, int id) { d->intHash.insert(sender, id); connect(sender, SIGNAL(destroyed()), this, SLOT(_q_senderDestroyed())); } /*! Adds a mapping so that when map() is signalled from the \a sender, the signal mapped(\a text ) is emitted. There may be at most one text for each sender. */ void KisSignalMapper::setMapping(QObject *sender, const QString &text) { d->stringHash.insert(sender, text); connect(sender, SIGNAL(destroyed()), this, SLOT(_q_senderDestroyed())); } /*! Adds a mapping so that when map() is signalled from the \a sender, the signal mapped(\a widget ) is emitted. There may be at most one widget for each sender. */ void KisSignalMapper::setMapping(QObject *sender, QWidget *widget) { d->widgetHash.insert(sender, widget); connect(sender, SIGNAL(destroyed()), this, SLOT(_q_senderDestroyed())); } /*! Adds a mapping so that when map() is signalled from the \a sender, the signal mapped(\a object ) is emitted. There may be at most one object for each sender. */ void KisSignalMapper::setMapping(QObject *sender, QObject *object) { d->objectHash.insert(sender, object); connect(sender, SIGNAL(destroyed()), this, SLOT(_q_senderDestroyed())); } /*! Returns the sender QObject that is associated with the \a id. \sa setMapping() */ QObject *KisSignalMapper::mapping(int id) const { return d->intHash.key(id); } /*! \overload mapping() */ QObject *KisSignalMapper::mapping(const QString &id) const { return d->stringHash.key(id); } /*! \overload mapping() Returns the sender QObject that is associated with the \a widget. */ QObject *KisSignalMapper::mapping(QWidget *widget) const { return d->widgetHash.key(widget); } /*! \overload mapping() Returns the sender QObject that is associated with the \a object. */ QObject *KisSignalMapper::mapping(QObject *object) const { return d->objectHash.key(object); } /*! Removes all mappings for \a sender. This is done automatically when mapped objects are destroyed. \note This does not disconnect any signals. If \a sender is not destroyed then this will need to be done explicitly if required. */ void KisSignalMapper::removeMappings(QObject *sender) { d->intHash.remove(sender); d->stringHash.remove(sender); d->widgetHash.remove(sender); d->objectHash.remove(sender); } /*! This slot emits signals based on which object sends signals to it. */ void KisSignalMapper::map() { map(sender()); } /*! This slot emits signals based on the \a sender object. */ void KisSignalMapper::map(QObject *sender) { if (d->intHash.contains(sender)) emit mapped(d->intHash.value(sender)); if (d->stringHash.contains(sender)) emit mapped(d->stringHash.value(sender)); if (d->widgetHash.contains(sender)) emit mapped(d->widgetHash.value(sender)); if (d->objectHash.contains(sender)) emit mapped(d->objectHash.value(sender)); } /*! \fn void KisSignalMapper::mapped(int i) This signal is emitted when map() is signalled from an object that has an integer mapping set. The object's mapped integer is passed in \a i. \sa setMapping() */ /*! \fn void KisSignalMapper::mapped(const QString &text) This signal is emitted when map() is signalled from an object that has a string mapping set. The object's mapped string is passed in \a text. \sa setMapping() */ /*! \fn void KisSignalMapper::mapped(QWidget *widget) This signal is emitted when map() is signalled from an object that has a widget mapping set. The object's mapped widget is passed in \a widget. \sa setMapping() */ /*! \fn void KisSignalMapper::mapped(QObject *object) This signal is emitted when map() is signalled from an object that has an object mapping set. The object provided by the map is passed in \a object. \sa setMapping() */ #include "moc_KisSignalMapper.cpp" diff --git a/libs/global/KisSignalMapper.h b/libs/global/KisSignalMapper.h index 354159dbac..5e19979049 100644 --- a/libs/global/KisSignalMapper.h +++ b/libs/global/KisSignalMapper.h @@ -1,255 +1,255 @@ /**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtCore module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 3 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL3 included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 3 requirements ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 2.0 or (at your option) the GNU General ** Public license version 3 or any later version approved by the KDE Free ** Qt Foundation. The licenses are as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-2.0.html and ** https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #ifndef KisSignalMapper_H #define KisSignalMapper_H #include #include "kritaglobal_export.h" #include /*! \class KisSignalMapper \inmodule QtCore \obsolete The recommended solution is connecting the signal to a lambda. \brief The KisSignalMapper class bundles signals from identifiable senders. \ingroup objectmodel This class collects a set of parameterless signals, and re-emits them with integer, string or widget parameters corresponding to the object that sent the signal. The class supports the mapping of particular strings or integers with particular objects using setMapping(). The objects' signals can then be connected to the map() slot which will emit the mapped() signal with the string or integer associated with the - original signalling object. Mappings can be removed later using + original signaling object. Mappings can be removed later using removeMappings(). Example: Suppose we want to create a custom widget that contains a group of buttons (like a tool palette). One approach is to connect each button's \c clicked() signal to its own custom slot; but in this example we want to connect all the buttons to a single slot and parameterize the slot by the button that was clicked. Here's the definition of a simple custom widget that has a single signal, \c clicked(), which is emitted with the text of the button that was clicked: \snippet KisSignalMapper/buttonwidget.h 0 \snippet KisSignalMapper/buttonwidget.h 1 The only function that we need to implement is the constructor: \snippet KisSignalMapper/buttonwidget.cpp 0 \snippet KisSignalMapper/buttonwidget.cpp 1 \snippet KisSignalMapper/buttonwidget.cpp 2 A list of texts is passed to the constructor. A signal mapper is constructed and for each text in the list a QPushButton is created. We connect each button's \c clicked() signal to the signal mapper's map() slot, and create a mapping in the signal mapper from each button to the button's text. Finally we connect the signal mapper's mapped() signal to the custom widget's \c clicked() signal. When the user clicks a button, the custom widget will emit a single \c clicked() signal whose argument is the text of the button the user clicked. This class was mostly useful before lambda functions could be used as slots. The example above can be rewritten simpler without KisSignalMapper by connecting to a lambda function. \snippet KisSignalMapper/buttonwidget.cpp 3 \sa QObject, QButtonGroup, QActionGroup */ class KRITAGLOBAL_EXPORT KisSignalMapper : public QObject { Q_OBJECT public: explicit KisSignalMapper(QObject *parent = nullptr); /*! Destroys the KisSignalMapper. */ ~KisSignalMapper(); /*! Adds a mapping so that when map() is signalled from the given \a sender, the signal mapped(\a id) is emitted. There may be at most one integer ID for each sender. \sa mapping() */ void setMapping(QObject *sender, int id); /*! Adds a mapping so that when map() is signalled from the \a sender, the signal mapped(\a text ) is emitted. There may be at most one text for each sender. */ void setMapping(QObject *sender, const QString &text); /*! Adds a mapping so that when map() is signalled from the \a sender, the signal mapped(\a widget ) is emitted. There may be at most one widget for each sender. */ void setMapping(QObject *sender, QWidget *widget); /*! Adds a mapping so that when map() is signalled from the \a sender, the signal mapped(\a object ) is emitted. There may be at most one object for each sender. */ void setMapping(QObject *sender, QObject *object); /*! Removes all mappings for \a sender. This is done automatically when mapped objects are destroyed. \note This does not disconnect any signals. If \a sender is not destroyed then this will need to be done explicitly if required. */ void removeMappings(QObject *sender); /*! \overload mapping() */ QObject *mapping(int id) const; /*! \overload mapping() Returns the sender QObject that is associated with the \a widget. */ QObject *mapping(const QString &text) const; /*! \overload mapping() Returns the sender QObject that is associated with the \a object. */ QObject *mapping(QWidget *widget) const; /*! Returns the sender QObject that is associated with the \a id. \sa setMapping() */ QObject *mapping(QObject *object) const; Q_SIGNALS: /*! \fn void KisSignalMapper::mapped(int i) This signal is emitted when map() is signalled from an object that has an integer mapping set. The object's mapped integer is passed in \a i. \sa setMapping() */ void mapped(int); /*! \fn void KisSignalMapper::mapped(const QString &text) This signal is emitted when map() is signalled from an object that has a string mapping set. The object's mapped string is passed in \a text. \sa setMapping() */ void mapped(const QString &); /*! \fn void KisSignalMapper::mapped(QWidget *widget) This signal is emitted when map() is signalled from an object that has a widget mapping set. The object's mapped widget is passed in \a widget. \sa setMapping() */ void mapped(QWidget *); /*! \fn void KisSignalMapper::mapped(QObject *object) This signal is emitted when map() is signalled from an object that has an object mapping set. The object provided by the map is passed in \a object. \sa setMapping() */ void mapped(QObject *); public Q_SLOTS: /*! This slot emits signals based on which object sends signals to it. */ void map(); /*! This slot emits signals based on the \a sender object. */ void map(QObject *sender); private: class Private; QScopedPointer d; Q_DISABLE_COPY(KisSignalMapper) Q_PRIVATE_SLOT(d, void _q_senderDestroyed()) }; #endif // KisSignalMapper_H diff --git a/libs/image/kis_image.cc b/libs/image/kis_image.cc index 4364c7cb19..8b112848fc 100644 --- a/libs/image/kis_image.cc +++ b/libs/image/kis_image.cc @@ -1,2068 +1,2068 @@ /* * Copyright (c) 2002 Patrick Julien * Copyright (c) 2007 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 "kis_image.h" #include // WORDS_BIGENDIAN #include #include #include #include #include #include #include #include #include #include #include "KoColorSpaceRegistry.h" #include "KoColor.h" #include "KoColorProfile.h" #include #include "KisProofingConfiguration.h" #include "kis_adjustment_layer.h" #include "kis_annotation.h" #include "kis_change_profile_visitor.h" #include "kis_colorspace_convert_visitor.h" #include "kis_count_visitor.h" #include "kis_filter_strategy.h" #include "kis_group_layer.h" #include "commands/kis_image_commands.h" #include "kis_layer.h" #include "kis_meta_data_merge_strategy_registry.h" #include "kis_name_server.h" #include "kis_paint_layer.h" #include "kis_projection_leaf.h" #include "kis_painter.h" #include "kis_selection.h" #include "kis_transaction.h" #include "kis_meta_data_merge_strategy.h" #include "kis_memory_statistics_server.h" #include "kis_image_config.h" #include "kis_update_scheduler.h" #include "kis_image_signal_router.h" #include "kis_image_animation_interface.h" #include "kis_stroke_strategy.h" #include "kis_simple_stroke_strategy.h" #include "kis_image_barrier_locker.h" #include "kis_undo_stores.h" #include "kis_legacy_undo_adapter.h" #include "kis_post_execution_undo_adapter.h" #include "kis_transform_worker.h" #include "kis_processing_applicator.h" #include "processing/kis_crop_processing_visitor.h" #include "processing/kis_crop_selections_processing_visitor.h" #include "processing/kis_transform_processing_visitor.h" #include "commands_new/kis_image_resize_command.h" #include "commands_new/kis_image_set_resolution_command.h" #include "commands_new/kis_activate_selection_mask_command.h" #include "kis_composite_progress_proxy.h" #include "kis_layer_composition.h" #include "kis_wrapped_rect.h" #include "kis_crop_saved_extra_data.h" #include "kis_layer_utils.h" #include "kis_lod_transform.h" #include "kis_suspend_projection_updates_stroke_strategy.h" #include "kis_sync_lod_cache_stroke_strategy.h" #include "kis_projection_updates_filter.h" #include "kis_layer_projection_plane.h" #include "kis_update_time_monitor.h" #include "tiles3/kis_lockless_stack.h" #include #include #include "kis_time_range.h" #include "KisRunnableBasedStrokeStrategy.h" #include "KisRunnableStrokeJobData.h" #include "KisRunnableStrokeJobUtils.h" #include "KisRunnableStrokeJobsInterface.h" // #define SANITY_CHECKS #ifdef SANITY_CHECKS #define SANITY_CHECK_LOCKED(name) \ if (!locked()) warnKrita() << "Locking policy failed:" << name \ << "has been called without the image" \ "being locked"; #else #define SANITY_CHECK_LOCKED(name) #endif struct KisImageSPStaticRegistrar { KisImageSPStaticRegistrar() { qRegisterMetaType("KisImageSP"); } }; static KisImageSPStaticRegistrar __registrar; class KisImage::KisImagePrivate { public: KisImagePrivate(KisImage *_q, qint32 w, qint32 h, const KoColorSpace *c, KisUndoStore *undo, KisImageAnimationInterface *_animationInterface) : q(_q) , lockedForReadOnly(false) , width(w) , height(h) , colorSpace(c ? c : KoColorSpaceRegistry::instance()->rgb8()) , nserver(1) , undoStore(undo ? undo : new KisDumbUndoStore()) , legacyUndoAdapter(undoStore.data(), _q) , postExecutionUndoAdapter(undoStore.data(), _q) , signalRouter(_q) , animationInterface(_animationInterface) , scheduler(_q, _q) , axesCenter(QPointF(0.5, 0.5)) { { KisImageConfig cfg(true); if (cfg.enableProgressReporting()) { scheduler.setProgressProxy(&compositeProgressProxy); } // Each of these lambdas defines a new factory function. scheduler.setLod0ToNStrokeStrategyFactory( [=](bool forgettable) { return KisLodSyncPair( new KisSyncLodCacheStrokeStrategy(KisImageWSP(q), forgettable), KisSyncLodCacheStrokeStrategy::createJobsData(KisImageWSP(q))); }); scheduler.setSuspendUpdatesStrokeStrategyFactory( [=]() { return KisSuspendResumePair( new KisSuspendProjectionUpdatesStrokeStrategy(KisImageWSP(q), true), KisSuspendProjectionUpdatesStrokeStrategy::createSuspendJobsData(KisImageWSP(q))); }); scheduler.setResumeUpdatesStrokeStrategyFactory( [=]() { return KisSuspendResumePair( new KisSuspendProjectionUpdatesStrokeStrategy(KisImageWSP(q), false), KisSuspendProjectionUpdatesStrokeStrategy::createResumeJobsData(KisImageWSP(q))); }); } connect(q, SIGNAL(sigImageModified()), KisMemoryStatisticsServer::instance(), SLOT(notifyImageChanged())); } ~KisImagePrivate() { /** * Stop animation interface. It may use the rootLayer. */ delete animationInterface; /** * First delete the nodes, while strokes * and undo are still alive */ rootLayer.clear(); } KisImage *q; quint32 lockCount = 0; bool lockedForReadOnly; qint32 width; qint32 height; double xres = 1.0; double yres = 1.0; const KoColorSpace * colorSpace; KisProofingConfigurationSP proofingConfig; KisSelectionSP deselectedGlobalSelection; KisGroupLayerSP rootLayer; // The layers are contained in here KisSelectionMaskSP targetOverlaySelectionMask; // the overlay switching stroke will try to switch into this mask KisSelectionMaskSP overlaySelectionMask; QList compositions; KisNodeSP isolatedRootNode; bool wrapAroundModePermitted = false; KisNameServer nserver; QScopedPointer undoStore; KisLegacyUndoAdapter legacyUndoAdapter; KisPostExecutionUndoAdapter postExecutionUndoAdapter; vKisAnnotationSP annotations; QAtomicInt disableUIUpdateSignals; KisLocklessStack savedDisabledUIUpdates; KisProjectionUpdatesFilterSP projectionUpdatesFilter; KisImageSignalRouter signalRouter; KisImageAnimationInterface *animationInterface; KisUpdateScheduler scheduler; QAtomicInt disableDirtyRequests; KisCompositeProgressProxy compositeProgressProxy; bool blockLevelOfDetail = false; QPointF axesCenter; bool allowMasksOnRootNode = false; bool tryCancelCurrentStrokeAsync(); void notifyProjectionUpdatedInPatches(const QRect &rc, QVector &jobs); }; KisImage::KisImage(KisUndoStore *undoStore, qint32 width, qint32 height, const KoColorSpace * colorSpace, const QString& name) : QObject(0) , KisShared() , m_d(new KisImagePrivate(this, width, height, colorSpace, undoStore, new KisImageAnimationInterface(this))) { // make sure KisImage belongs to the GUI thread moveToThread(qApp->thread()); connect(this, SIGNAL(sigInternalStopIsolatedModeRequested()), SLOT(stopIsolatedMode())); setObjectName(name); setRootLayer(new KisGroupLayer(this, "root", OPACITY_OPAQUE_U8)); } KisImage::~KisImage() { dbgImage << "deleting kisimage" << objectName(); /** * Request the tools to end currently running strokes */ waitForDone(); delete m_d; disconnect(); // in case Qt gets confused } KisImageSP KisImage::fromQImage(const QImage &image, KisUndoStore *undoStore) { const KoColorSpace *colorSpace = 0; switch (image.format()) { case QImage::Format_Invalid: case QImage::Format_Mono: case QImage::Format_MonoLSB: colorSpace = KoColorSpaceRegistry::instance()->graya8(); break; case QImage::Format_Indexed8: case QImage::Format_RGB32: case QImage::Format_ARGB32: case QImage::Format_ARGB32_Premultiplied: colorSpace = KoColorSpaceRegistry::instance()->rgb8(); break; case QImage::Format_RGB16: colorSpace = KoColorSpaceRegistry::instance()->rgb16(); break; case QImage::Format_ARGB8565_Premultiplied: case QImage::Format_RGB666: case QImage::Format_ARGB6666_Premultiplied: case QImage::Format_RGB555: case QImage::Format_ARGB8555_Premultiplied: case QImage::Format_RGB888: case QImage::Format_RGB444: case QImage::Format_ARGB4444_Premultiplied: case QImage::Format_RGBX8888: case QImage::Format_RGBA8888: case QImage::Format_RGBA8888_Premultiplied: colorSpace = KoColorSpaceRegistry::instance()->rgb8(); break; case QImage::Format_BGR30: case QImage::Format_A2BGR30_Premultiplied: case QImage::Format_RGB30: case QImage::Format_A2RGB30_Premultiplied: colorSpace = KoColorSpaceRegistry::instance()->rgb8(); break; case QImage::Format_Alpha8: colorSpace = KoColorSpaceRegistry::instance()->alpha8(); break; case QImage::Format_Grayscale8: colorSpace = KoColorSpaceRegistry::instance()->graya8(); break; #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) case QImage::Format_Grayscale16: colorSpace = KoColorSpaceRegistry::instance()->graya16(); break; #endif #if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) case QImage::Format_RGBX64: case QImage::Format_RGBA64: case QImage::Format_RGBA64_Premultiplied: colorSpace = KoColorSpaceRegistry::instance()->colorSpace(RGBAColorModelID.id(), Float32BitsColorDepthID.id(), 0); break; #endif default: colorSpace = 0; } KisImageSP img = new KisImage(undoStore, image.width(), image.height(), colorSpace, i18n("Imported Image")); KisPaintLayerSP layer = new KisPaintLayer(img, img->nextLayerName(), 255); layer->paintDevice()->convertFromQImage(image, 0, 0, 0); img->addNode(layer.data(), img->rootLayer().data()); return img; } KisImage *KisImage::clone(bool exactCopy) { return new KisImage(*this, 0, exactCopy); } void KisImage::copyFromImage(const KisImage &rhs) { copyFromImageImpl(rhs, REPLACE); } void KisImage::copyFromImageImpl(const KisImage &rhs, int policy) { // make sure we choose exactly one from REPLACE and CONSTRUCT KIS_ASSERT_RECOVER_RETURN((policy & REPLACE) != (policy & CONSTRUCT)); // only when replacing do we need to emit signals #define EMIT_IF_NEEDED if (!(policy & REPLACE)) {} else emit if (policy & REPLACE) { // if we are constructing the image, these are already set if (m_d->width != rhs.width() || m_d->height != rhs.height()) { m_d->width = rhs.width(); m_d->height = rhs.height(); emit sigSizeChanged(QPointF(), QPointF()); } if (m_d->colorSpace != rhs.colorSpace()) { m_d->colorSpace = rhs.colorSpace(); emit sigColorSpaceChanged(m_d->colorSpace); } } // from KisImage::KisImage(const KisImage &, KisUndoStore *, bool) setObjectName(rhs.objectName()); if (m_d->xres != rhs.m_d->xres || m_d->yres != rhs.m_d->yres) { m_d->xres = rhs.m_d->xres; m_d->yres = rhs.m_d->yres; EMIT_IF_NEEDED sigResolutionChanged(m_d->xres, m_d->yres); } m_d->allowMasksOnRootNode = rhs.m_d->allowMasksOnRootNode; if (rhs.m_d->proofingConfig) { KisProofingConfigurationSP proofingConfig(new KisProofingConfiguration(*rhs.m_d->proofingConfig)); if (policy & REPLACE) { setProofingConfiguration(proofingConfig); } else { m_d->proofingConfig = proofingConfig; } } KisNodeSP newRoot = rhs.root()->clone(); newRoot->setGraphListener(this); newRoot->setImage(this); m_d->rootLayer = dynamic_cast(newRoot.data()); setRoot(newRoot); bool exactCopy = policy & EXACT_COPY; if (exactCopy || rhs.m_d->isolatedRootNode) { QQueue linearizedNodes; KisLayerUtils::recursiveApplyNodes(rhs.root(), [&linearizedNodes](KisNodeSP node) { linearizedNodes.enqueue(node); }); KisLayerUtils::recursiveApplyNodes(newRoot, [&linearizedNodes, exactCopy, &rhs, this](KisNodeSP node) { KisNodeSP refNode = linearizedNodes.dequeue(); if (exactCopy) { node->setUuid(refNode->uuid()); } if (rhs.m_d->isolatedRootNode && rhs.m_d->isolatedRootNode == refNode) { m_d->isolatedRootNode = node; } }); } KisLayerUtils::recursiveApplyNodes(newRoot, [](KisNodeSP node) { dbgImage << "Node: " << (void *)node.data(); }); m_d->compositions.clear(); Q_FOREACH (KisLayerCompositionSP comp, rhs.m_d->compositions) { m_d->compositions << toQShared(new KisLayerComposition(*comp, this)); } EMIT_IF_NEEDED sigLayersChangedAsync(); m_d->nserver = rhs.m_d->nserver; vKisAnnotationSP newAnnotations; Q_FOREACH (KisAnnotationSP annotation, rhs.m_d->annotations) { newAnnotations << annotation->clone(); } m_d->annotations = newAnnotations; KIS_ASSERT_RECOVER_NOOP(!rhs.m_d->projectionUpdatesFilter); KIS_ASSERT_RECOVER_NOOP(!rhs.m_d->disableUIUpdateSignals); KIS_ASSERT_RECOVER_NOOP(!rhs.m_d->disableDirtyRequests); m_d->blockLevelOfDetail = rhs.m_d->blockLevelOfDetail; /** * The overlay device is not inherited when cloning the image! */ if (rhs.m_d->overlaySelectionMask) { const QRect dirtyRect = rhs.m_d->overlaySelectionMask->extent(); m_d->rootLayer->setDirty(dirtyRect); } #undef EMIT_IF_NEEDED } KisImage::KisImage(const KisImage& rhs, KisUndoStore *undoStore, bool exactCopy) : KisNodeFacade(), KisNodeGraphListener(), KisShared(), m_d(new KisImagePrivate(this, rhs.width(), rhs.height(), rhs.colorSpace(), undoStore ? undoStore : new KisDumbUndoStore(), new KisImageAnimationInterface(*rhs.animationInterface(), this))) { // make sure KisImage belongs to the GUI thread moveToThread(qApp->thread()); connect(this, SIGNAL(sigInternalStopIsolatedModeRequested()), SLOT(stopIsolatedMode())); copyFromImageImpl(rhs, CONSTRUCT | (exactCopy ? EXACT_COPY : 0)); } void KisImage::aboutToAddANode(KisNode *parent, int index) { KisNodeGraphListener::aboutToAddANode(parent, index); SANITY_CHECK_LOCKED("aboutToAddANode"); } void KisImage::nodeHasBeenAdded(KisNode *parent, int index) { KisNodeGraphListener::nodeHasBeenAdded(parent, index); SANITY_CHECK_LOCKED("nodeHasBeenAdded"); m_d->signalRouter.emitNodeHasBeenAdded(parent, index); } void KisImage::aboutToRemoveANode(KisNode *parent, int index) { KisNodeSP deletedNode = parent->at(index); if (!dynamic_cast(deletedNode.data()) && deletedNode == m_d->isolatedRootNode) { emit sigInternalStopIsolatedModeRequested(); } KisNodeGraphListener::aboutToRemoveANode(parent, index); SANITY_CHECK_LOCKED("aboutToRemoveANode"); m_d->signalRouter.emitAboutToRemoveANode(parent, index); } void KisImage::nodeChanged(KisNode* node) { KisNodeGraphListener::nodeChanged(node); requestStrokeEnd(); m_d->signalRouter.emitNodeChanged(node); } void KisImage::invalidateAllFrames() { invalidateFrames(KisTimeRange::infinite(0), QRect()); } void KisImage::setOverlaySelectionMask(KisSelectionMaskSP mask) { if (m_d->targetOverlaySelectionMask == mask) return; m_d->targetOverlaySelectionMask = mask; struct UpdateOverlaySelectionStroke : public KisSimpleStrokeStrategy { UpdateOverlaySelectionStroke(KisImageSP image) : KisSimpleStrokeStrategy("update-overlay-selection-mask", kundo2_noi18n("update-overlay-selection-mask")), m_image(image) { this->enableJob(JOB_INIT, true, KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); setClearsRedoOnStart(false); } void initStrokeCallback() { KisSelectionMaskSP oldMask = m_image->m_d->overlaySelectionMask; KisSelectionMaskSP newMask = m_image->m_d->targetOverlaySelectionMask; if (oldMask == newMask) return; KIS_SAFE_ASSERT_RECOVER_RETURN(!newMask || newMask->graphListener() == m_image); m_image->m_d->overlaySelectionMask = newMask; if (oldMask || newMask) { m_image->m_d->rootLayer->notifyChildMaskChanged(); } if (oldMask) { m_image->m_d->rootLayer->setDirtyDontResetAnimationCache(oldMask->extent()); } if (newMask) { newMask->setDirty(); } m_image->undoAdapter()->emitSelectionChanged(); } private: KisImageSP m_image; }; KisStrokeId id = startStroke(new UpdateOverlaySelectionStroke(this)); endStroke(id); } KisSelectionMaskSP KisImage::overlaySelectionMask() const { return m_d->overlaySelectionMask; } bool KisImage::hasOverlaySelectionMask() const { return m_d->overlaySelectionMask; } KisSelectionSP KisImage::globalSelection() const { KisSelectionMaskSP selectionMask = m_d->rootLayer->selectionMask(); if (selectionMask) { return selectionMask->selection(); } else { return 0; } } void KisImage::setGlobalSelection(KisSelectionSP globalSelection) { KisSelectionMaskSP selectionMask = m_d->rootLayer->selectionMask(); if (!globalSelection) { if (selectionMask) { removeNode(selectionMask); } } else { if (!selectionMask) { selectionMask = new KisSelectionMask(this); selectionMask->initSelection(m_d->rootLayer); addNode(selectionMask); // If we do not set the selection now, the setActive call coming next // can be very, very expensive, depending on the size of the image. selectionMask->setSelection(globalSelection); selectionMask->setActive(true); } else { selectionMask->setSelection(globalSelection); } KIS_SAFE_ASSERT_RECOVER_NOOP(m_d->rootLayer->childCount() > 0); KIS_SAFE_ASSERT_RECOVER_NOOP(m_d->rootLayer->selectionMask()); } m_d->deselectedGlobalSelection = 0; m_d->legacyUndoAdapter.emitSelectionChanged(); } void KisImage::deselectGlobalSelection() { KisSelectionSP savedSelection = globalSelection(); setGlobalSelection(0); m_d->deselectedGlobalSelection = savedSelection; } bool KisImage::canReselectGlobalSelection() { return m_d->deselectedGlobalSelection; } void KisImage::reselectGlobalSelection() { if(m_d->deselectedGlobalSelection) { setGlobalSelection(m_d->deselectedGlobalSelection); } } QString KisImage::nextLayerName(const QString &_baseName) const { QString baseName = _baseName; if (m_d->nserver.currentSeed() == 0) { m_d->nserver.number(); return i18n("background"); } if (baseName.isEmpty()) { baseName = i18n("Layer"); } return QString("%1 %2").arg(baseName).arg(m_d->nserver.number()); } void KisImage::rollBackLayerName() { m_d->nserver.rollback(); } KisCompositeProgressProxy* KisImage::compositeProgressProxy() { return &m_d->compositeProgressProxy; } bool KisImage::locked() const { return m_d->lockCount != 0; } void KisImage::barrierLock(bool readOnly) { if (!locked()) { requestStrokeEnd(); m_d->scheduler.barrierLock(); m_d->lockedForReadOnly = readOnly; } else { m_d->lockedForReadOnly &= readOnly; } m_d->lockCount++; } bool KisImage::tryBarrierLock(bool readOnly) { bool result = true; if (!locked()) { result = m_d->scheduler.tryBarrierLock(); m_d->lockedForReadOnly = readOnly; } if (result) { m_d->lockCount++; m_d->lockedForReadOnly &= readOnly; } return result; } bool KisImage::isIdle(bool allowLocked) { return (allowLocked || !locked()) && m_d->scheduler.isIdle(); } void KisImage::lock() { if (!locked()) { requestStrokeEnd(); m_d->scheduler.lock(); } m_d->lockCount++; m_d->lockedForReadOnly = false; } void KisImage::unlock() { Q_ASSERT(locked()); if (locked()) { m_d->lockCount--; if (m_d->lockCount == 0) { m_d->scheduler.unlock(!m_d->lockedForReadOnly); } } } void KisImage::blockUpdates() { m_d->scheduler.blockUpdates(); } void KisImage::unblockUpdates() { m_d->scheduler.unblockUpdates(); } void KisImage::setSize(const QSize& size) { m_d->width = size.width(); m_d->height = size.height(); } void KisImage::resizeImageImpl(const QRect& newRect, bool cropLayers) { if (newRect == bounds() && !cropLayers) return; KUndo2MagicString actionName = cropLayers ? kundo2_i18n("Crop Image") : kundo2_i18n("Resize Image"); KisImageSignalVector emitSignals; emitSignals << ComplexSizeChangedSignal(newRect, newRect.size()); emitSignals << ModifiedSignal; KisCropSavedExtraData *extraData = new KisCropSavedExtraData(cropLayers ? KisCropSavedExtraData::CROP_IMAGE : KisCropSavedExtraData::RESIZE_IMAGE, newRect); KisProcessingApplicator applicator(this, m_d->rootLayer, KisProcessingApplicator::RECURSIVE | KisProcessingApplicator::NO_UI_UPDATES, emitSignals, actionName, extraData); if (cropLayers || !newRect.topLeft().isNull()) { KisProcessingVisitorSP visitor = new KisCropProcessingVisitor(newRect, cropLayers, true); applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); } applicator.applyCommand(new KisImageResizeCommand(this, newRect.size())); applicator.end(); } void KisImage::resizeImage(const QRect& newRect) { resizeImageImpl(newRect, false); } void KisImage::cropImage(const QRect& newRect) { resizeImageImpl(newRect, true); } void KisImage::cropNode(KisNodeSP node, const QRect& newRect) { bool isLayer = qobject_cast(node.data()); KUndo2MagicString actionName = isLayer ? kundo2_i18n("Crop Layer") : kundo2_i18n("Crop Mask"); KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; KisCropSavedExtraData *extraData = new KisCropSavedExtraData(KisCropSavedExtraData::CROP_LAYER, newRect, node); KisProcessingApplicator applicator(this, node, KisProcessingApplicator::RECURSIVE, emitSignals, actionName, extraData); KisProcessingVisitorSP visitor = new KisCropProcessingVisitor(newRect, true, false); applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); applicator.end(); } void KisImage::scaleImage(const QSize &size, qreal xres, qreal yres, KisFilterStrategy *filterStrategy) { bool resolutionChanged = xres != xRes() && yres != yRes(); bool sizeChanged = size != this->size(); if (!resolutionChanged && !sizeChanged) return; KisImageSignalVector emitSignals; if (resolutionChanged) emitSignals << ResolutionChangedSignal; if (sizeChanged) emitSignals << ComplexSizeChangedSignal(bounds(), size); emitSignals << ModifiedSignal; KUndo2MagicString actionName = sizeChanged ? kundo2_i18n("Scale Image") : kundo2_i18n("Change Image Resolution"); KisProcessingApplicator::ProcessingFlags signalFlags = (resolutionChanged || sizeChanged) ? KisProcessingApplicator::NO_UI_UPDATES : KisProcessingApplicator::NONE; KisProcessingApplicator applicator(this, m_d->rootLayer, KisProcessingApplicator::RECURSIVE | signalFlags, emitSignals, actionName); qreal sx = qreal(size.width()) / this->size().width(); qreal sy = qreal(size.height()) / this->size().height(); QTransform shapesCorrection; if (resolutionChanged) { shapesCorrection = QTransform::fromScale(xRes() / xres, yRes() / yres); } KisProcessingVisitorSP visitor = new KisTransformProcessingVisitor(sx, sy, 0, 0, QPointF(), 0, 0, 0, filterStrategy, shapesCorrection); applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); if (resolutionChanged) { KUndo2Command *parent = new KisResetShapesCommand(m_d->rootLayer); new KisImageSetResolutionCommand(this, xres, yres, parent); applicator.applyCommand(parent); } if (sizeChanged) { applicator.applyCommand(new KisImageResizeCommand(this, size)); } applicator.end(); } void KisImage::scaleNode(KisNodeSP node, const QPointF ¢er, qreal scaleX, qreal scaleY, KisFilterStrategy *filterStrategy, KisSelectionSP selection) { KUndo2MagicString actionName(kundo2_i18n("Scale Layer")); KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; QPointF offset; { KisTransformWorker worker(0, scaleX, scaleY, 0, 0, 0, 0, 0.0, 0, 0, 0, 0); QTransform transform = worker.transform(); offset = center - transform.map(center); } KisProcessingApplicator applicator(this, node, KisProcessingApplicator::RECURSIVE, emitSignals, actionName); KisTransformProcessingVisitor *visitor = new KisTransformProcessingVisitor(scaleX, scaleY, 0, 0, QPointF(), 0, offset.x(), offset.y(), filterStrategy); visitor->setSelection(selection); if (selection) { applicator.applyVisitor(visitor, KisStrokeJobData::CONCURRENT); } else { applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); } applicator.end(); } void KisImage::rotateImpl(const KUndo2MagicString &actionName, KisNodeSP rootNode, double radians, bool resizeImage, KisSelectionSP selection) { // we can either transform (and resize) the whole image or // transform a selection, we cannot do both at the same time KIS_SAFE_ASSERT_RECOVER(!(bool(selection) && resizeImage)) { selection = 0; } const QRect baseBounds = resizeImage ? bounds() : selection ? selection->selectedExactRect() : rootNode->exactBounds(); QPointF offset; QSize newSize; { KisTransformWorker worker(0, 1.0, 1.0, 0, 0, 0, 0, radians, 0, 0, 0, 0); QTransform transform = worker.transform(); if (resizeImage) { QRect newRect = transform.mapRect(baseBounds); newSize = newRect.size(); offset = -newRect.topLeft(); } else { QPointF origin = QRectF(baseBounds).center(); newSize = size(); offset = -(transform.map(origin) - origin); } } bool sizeChanged = resizeImage && (newSize.width() != baseBounds.width() || newSize.height() != baseBounds.height()); // These signals will be emitted after processing is done KisImageSignalVector emitSignals; if (sizeChanged) emitSignals << ComplexSizeChangedSignal(baseBounds, newSize); emitSignals << ModifiedSignal; // These flags determine whether updates are transferred to the UI during processing KisProcessingApplicator::ProcessingFlags signalFlags = sizeChanged ? KisProcessingApplicator::NO_UI_UPDATES : KisProcessingApplicator::NONE; KisProcessingApplicator applicator(this, rootNode, KisProcessingApplicator::RECURSIVE | signalFlags, emitSignals, actionName); KisFilterStrategy *filter = KisFilterStrategyRegistry::instance()->value("Bicubic"); KisTransformProcessingVisitor *visitor = new KisTransformProcessingVisitor(1.0, 1.0, 0.0, 0.0, QPointF(), radians, offset.x(), offset.y(), filter); if (selection) { visitor->setSelection(selection); } if (selection) { applicator.applyVisitor(visitor, KisStrokeJobData::CONCURRENT); } else { applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); } if (sizeChanged) { applicator.applyCommand(new KisImageResizeCommand(this, newSize)); } applicator.end(); } void KisImage::rotateImage(double radians) { rotateImpl(kundo2_i18n("Rotate Image"), root(), radians, true, 0); } void KisImage::rotateNode(KisNodeSP node, double radians, KisSelectionSP selection) { if (node->inherits("KisMask")) { rotateImpl(kundo2_i18n("Rotate Mask"), node, radians, false, selection); } else { rotateImpl(kundo2_i18n("Rotate Layer"), node, radians, false, selection); } } void KisImage::shearImpl(const KUndo2MagicString &actionName, KisNodeSP rootNode, bool resizeImage, double angleX, double angleY, KisSelectionSP selection) { const QRect baseBounds = resizeImage ? bounds() : selection ? selection->selectedExactRect() : rootNode->exactBounds(); const QPointF origin = QRectF(baseBounds).center(); //angleX, angleY are in degrees const qreal pi = 3.1415926535897932385; const qreal deg2rad = pi / 180.0; qreal tanX = tan(angleX * deg2rad); qreal tanY = tan(angleY * deg2rad); QPointF offset; QSize newSize; { KisTransformWorker worker(0, 1.0, 1.0, tanX, tanY, origin.x(), origin.y(), 0, 0, 0, 0, 0); QRect newRect = worker.transform().mapRect(baseBounds); newSize = newRect.size(); if (resizeImage) offset = -newRect.topLeft(); } if (newSize == baseBounds.size()) return; KisImageSignalVector emitSignals; if (resizeImage) emitSignals << ComplexSizeChangedSignal(baseBounds, newSize); emitSignals << ModifiedSignal; KisProcessingApplicator::ProcessingFlags signalFlags = KisProcessingApplicator::RECURSIVE; if (resizeImage) signalFlags |= KisProcessingApplicator::NO_UI_UPDATES; KisProcessingApplicator applicator(this, rootNode, signalFlags, emitSignals, actionName); KisFilterStrategy *filter = KisFilterStrategyRegistry::instance()->value("Bilinear"); KisTransformProcessingVisitor *visitor = new KisTransformProcessingVisitor(1.0, 1.0, tanX, tanY, origin, 0, offset.x(), offset.y(), filter); if (selection) { visitor->setSelection(selection); } if (selection) { applicator.applyVisitor(visitor, KisStrokeJobData::CONCURRENT); } else { applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); } if (resizeImage) { applicator.applyCommand(new KisImageResizeCommand(this, newSize)); } applicator.end(); } void KisImage::shearNode(KisNodeSP node, double angleX, double angleY, KisSelectionSP selection) { if (node->inherits("KisMask")) { shearImpl(kundo2_i18n("Shear Mask"), node, false, angleX, angleY, selection); } else { shearImpl(kundo2_i18n("Shear Layer"), node, false, angleX, angleY, selection); } } void KisImage::shear(double angleX, double angleY) { shearImpl(kundo2_i18n("Shear Image"), m_d->rootLayer, true, angleX, angleY, 0); } void KisImage::convertImageColorSpace(const KoColorSpace *dstColorSpace, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) { if (!dstColorSpace) return; const KoColorSpace *srcColorSpace = m_d->colorSpace; undoAdapter()->beginMacro(kundo2_i18n("Convert Image Color Space")); undoAdapter()->addCommand(new KisImageLockCommand(KisImageWSP(this), true)); undoAdapter()->addCommand(new KisImageSetProjectionColorSpaceCommand(KisImageWSP(this), dstColorSpace)); KisColorSpaceConvertVisitor visitor(this, srcColorSpace, dstColorSpace, renderingIntent, conversionFlags); m_d->rootLayer->accept(visitor); undoAdapter()->addCommand(new KisImageLockCommand(KisImageWSP(this), false)); undoAdapter()->endMacro(); setModified(); } bool KisImage::assignImageProfile(const KoColorProfile *profile) { if (!profile) return false; const KoColorSpace *dstCs = KoColorSpaceRegistry::instance()->colorSpace(colorSpace()->colorModelId().id(), colorSpace()->colorDepthId().id(), profile); const KoColorSpace *srcCs = colorSpace(); if (!dstCs) return false; m_d->colorSpace = dstCs; KisChangeProfileVisitor visitor(srcCs, dstCs); bool retval = m_d->rootLayer->accept(visitor); m_d->signalRouter.emitNotification(ProfileChangedSignal); return retval; } void KisImage::convertProjectionColorSpace(const KoColorSpace *dstColorSpace) { if (*m_d->colorSpace == *dstColorSpace) return; undoAdapter()->beginMacro(kundo2_i18n("Convert Projection Color Space")); undoAdapter()->addCommand(new KisImageLockCommand(KisImageWSP(this), true)); undoAdapter()->addCommand(new KisImageSetProjectionColorSpaceCommand(KisImageWSP(this), dstColorSpace)); undoAdapter()->addCommand(new KisImageLockCommand(KisImageWSP(this), false)); undoAdapter()->endMacro(); setModified(); } void KisImage::setProjectionColorSpace(const KoColorSpace * colorSpace) { m_d->colorSpace = colorSpace; m_d->rootLayer->resetCache(); m_d->signalRouter.emitNotification(ColorSpaceChangedSignal); } const KoColorSpace * KisImage::colorSpace() const { return m_d->colorSpace; } const KoColorProfile * KisImage::profile() const { return colorSpace()->profile(); } double KisImage::xRes() const { return m_d->xres; } double KisImage::yRes() const { return m_d->yres; } void KisImage::setResolution(double xres, double yres) { m_d->xres = xres; m_d->yres = yres; m_d->signalRouter.emitNotification(ResolutionChangedSignal); } QPointF KisImage::documentToPixel(const QPointF &documentCoord) const { return QPointF(documentCoord.x() * xRes(), documentCoord.y() * yRes()); } QPoint KisImage::documentToImagePixelFloored(const QPointF &documentCoord) const { QPointF pixelCoord = documentToPixel(documentCoord); return QPoint(qFloor(pixelCoord.x()), qFloor(pixelCoord.y())); } QRectF KisImage::documentToPixel(const QRectF &documentRect) const { return QRectF(documentToPixel(documentRect.topLeft()), documentToPixel(documentRect.bottomRight())); } QPointF KisImage::pixelToDocument(const QPointF &pixelCoord) const { return QPointF(pixelCoord.x() / xRes(), pixelCoord.y() / yRes()); } QPointF KisImage::pixelToDocument(const QPoint &pixelCoord) const { return QPointF((pixelCoord.x() + 0.5) / xRes(), (pixelCoord.y() + 0.5) / yRes()); } QRectF KisImage::pixelToDocument(const QRectF &pixelCoord) const { return QRectF(pixelToDocument(pixelCoord.topLeft()), pixelToDocument(pixelCoord.bottomRight())); } qint32 KisImage::width() const { return m_d->width; } qint32 KisImage::height() const { return m_d->height; } KisGroupLayerSP KisImage::rootLayer() const { Q_ASSERT(m_d->rootLayer); return m_d->rootLayer; } KisPaintDeviceSP KisImage::projection() const { if (m_d->isolatedRootNode) { return m_d->isolatedRootNode->projection(); } Q_ASSERT(m_d->rootLayer); KisPaintDeviceSP projection = m_d->rootLayer->projection(); Q_ASSERT(projection); return projection; } qint32 KisImage::nlayers() const { QStringList list; list << "KisLayer"; KisCountVisitor visitor(list, KoProperties()); m_d->rootLayer->accept(visitor); return visitor.count(); } qint32 KisImage::nHiddenLayers() const { QStringList list; list << "KisLayer"; KoProperties properties; properties.setProperty("visible", false); KisCountVisitor visitor(list, properties); m_d->rootLayer->accept(visitor); return visitor.count(); } void KisImage::flatten(KisNodeSP activeNode) { KisLayerUtils::flattenImage(this, activeNode); } void KisImage::mergeMultipleLayers(QList mergedNodes, KisNodeSP putAfter) { if (!KisLayerUtils::tryMergeSelectionMasks(this, mergedNodes, putAfter)) { KisLayerUtils::mergeMultipleLayers(this, mergedNodes, putAfter); } } void KisImage::mergeDown(KisLayerSP layer, const KisMetaData::MergeStrategy* strategy) { KisLayerUtils::mergeDown(this, layer, strategy); } void KisImage::flattenLayer(KisLayerSP layer) { KisLayerUtils::flattenLayer(this, layer); } void KisImage::setModified() { m_d->signalRouter.emitNotification(ModifiedSignal); } QImage KisImage::convertToQImage(QRect imageRect, const KoColorProfile * profile) { qint32 x; qint32 y; qint32 w; qint32 h; imageRect.getRect(&x, &y, &w, &h); return convertToQImage(x, y, w, h, profile); } QImage KisImage::convertToQImage(qint32 x, qint32 y, qint32 w, qint32 h, const KoColorProfile * profile) { KisPaintDeviceSP dev = projection(); if (!dev) return QImage(); QImage image = dev->convertToQImage(const_cast(profile), x, y, w, h, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); return image; } QImage KisImage::convertToQImage(const QSize& scaledImageSize, const KoColorProfile *profile) { if (scaledImageSize.isEmpty()) { return QImage(); } KisPaintDeviceSP dev = new KisPaintDevice(colorSpace()); KisPainter gc; gc.copyAreaOptimized(QPoint(0, 0), projection(), dev, bounds()); gc.end(); double scaleX = qreal(scaledImageSize.width()) / width(); double scaleY = qreal(scaledImageSize.height()) / height(); QPointer updater = new KoDummyUpdater(); KisTransformWorker worker(dev, scaleX, scaleY, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, updater, KisFilterStrategyRegistry::instance()->value("Bicubic")); worker.run(); delete updater; return dev->convertToQImage(profile); } void KisImage::notifyLayersChanged() { m_d->signalRouter.emitNotification(LayersChangedSignal); } QRect KisImage::bounds() const { return QRect(0, 0, width(), height()); } QRect KisImage::effectiveLodBounds() const { QRect boundRect = bounds(); const int lod = currentLevelOfDetail(); if (lod > 0) { KisLodTransform t(lod); boundRect = t.map(boundRect); } return boundRect; } KisPostExecutionUndoAdapter* KisImage::postExecutionUndoAdapter() const { const int lod = currentLevelOfDetail(); return lod > 0 ? m_d->scheduler.lodNPostExecutionUndoAdapter() : &m_d->postExecutionUndoAdapter; } const KUndo2Command* KisImage::lastExecutedCommand() const { return m_d->undoStore->presentCommand(); } void KisImage::setUndoStore(KisUndoStore *undoStore) { m_d->legacyUndoAdapter.setUndoStore(undoStore); m_d->postExecutionUndoAdapter.setUndoStore(undoStore); m_d->undoStore.reset(undoStore); } KisUndoStore* KisImage::undoStore() { return m_d->undoStore.data(); } KisUndoAdapter* KisImage::undoAdapter() const { return &m_d->legacyUndoAdapter; } void KisImage::setDefaultProjectionColor(const KoColor &color) { KIS_ASSERT_RECOVER_RETURN(m_d->rootLayer); m_d->rootLayer->setDefaultProjectionColor(color); } KoColor KisImage::defaultProjectionColor() const { KIS_ASSERT_RECOVER(m_d->rootLayer) { return KoColor(Qt::transparent, m_d->colorSpace); } return m_d->rootLayer->defaultProjectionColor(); } void KisImage::setRootLayer(KisGroupLayerSP rootLayer) { emit sigInternalStopIsolatedModeRequested(); KoColor defaultProjectionColor(Qt::transparent, m_d->colorSpace); if (m_d->rootLayer) { m_d->rootLayer->setGraphListener(0); m_d->rootLayer->disconnect(); KisPaintDeviceSP original = m_d->rootLayer->original(); defaultProjectionColor = original->defaultPixel(); } m_d->rootLayer = rootLayer; m_d->rootLayer->disconnect(); m_d->rootLayer->setGraphListener(this); m_d->rootLayer->setImage(this); setRoot(m_d->rootLayer.data()); this->setDefaultProjectionColor(defaultProjectionColor); } void KisImage::addAnnotation(KisAnnotationSP annotation) { // Find the icc annotation, if there is one vKisAnnotationSP_it it = m_d->annotations.begin(); while (it != m_d->annotations.end()) { if ((*it)->type() == annotation->type()) { *it = annotation; return; } ++it; } m_d->annotations.push_back(annotation); } KisAnnotationSP KisImage::annotation(const QString& type) { vKisAnnotationSP_it it = m_d->annotations.begin(); while (it != m_d->annotations.end()) { if ((*it)->type() == type) { return *it; } ++it; } return KisAnnotationSP(0); } void KisImage::removeAnnotation(const QString& type) { vKisAnnotationSP_it it = m_d->annotations.begin(); while (it != m_d->annotations.end()) { if ((*it)->type() == type) { m_d->annotations.erase(it); return; } ++it; } } vKisAnnotationSP_it KisImage::beginAnnotations() { return m_d->annotations.begin(); } vKisAnnotationSP_it KisImage::endAnnotations() { return m_d->annotations.end(); } void KisImage::notifyAboutToBeDeleted() { emit sigAboutToBeDeleted(); } KisImageSignalRouter* KisImage::signalRouter() { return &m_d->signalRouter; } void KisImage::waitForDone() { requestStrokeEnd(); m_d->scheduler.waitForDone(); } KisStrokeId KisImage::startStroke(KisStrokeStrategy *strokeStrategy) { /** * Ask open strokes to end gracefully. All the strokes clients * (including the one calling this method right now) will get * a notification that they should probably end their strokes. * However this is purely their choice whether to end a stroke * or not. */ if (strokeStrategy->requestsOtherStrokesToEnd()) { requestStrokeEnd(); } /** * Some of the strokes can cancel their work with undoing all the * changes they did to the paint devices. The problem is that undo * stack will know nothing about it. Therefore, just notify it * explicitly */ if (strokeStrategy->clearsRedoOnStart()) { m_d->undoStore->purgeRedoState(); } return m_d->scheduler.startStroke(strokeStrategy); } void KisImage::KisImagePrivate::notifyProjectionUpdatedInPatches(const QRect &rc, QVector &jobs) { KisImageConfig imageConfig(true); int patchWidth = imageConfig.updatePatchWidth(); int patchHeight = imageConfig.updatePatchHeight(); for (int y = 0; y < rc.height(); y += patchHeight) { for (int x = 0; x < rc.width(); x += patchWidth) { QRect patchRect(x, y, patchWidth, patchHeight); patchRect &= rc; KritaUtils::addJobConcurrent(jobs, std::bind(&KisImage::notifyProjectionUpdated, q, patchRect)); } } } bool KisImage::startIsolatedMode(KisNodeSP node) { struct StartIsolatedModeStroke : public KisRunnableBasedStrokeStrategy { StartIsolatedModeStroke(KisNodeSP node, KisImageSP image) : KisRunnableBasedStrokeStrategy("start-isolated-mode", kundo2_noi18n("start-isolated-mode")), m_node(node), m_image(image) { this->enableJob(JOB_INIT, true, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); this->enableJob(JOB_DOSTROKE, true); setClearsRedoOnStart(false); } void initStrokeCallback() { // pass-though node don't have any projection prepared, so we should // explicitly regenerate it before activating isolated mode. m_node->projectionLeaf()->explicitlyRegeneratePassThroughProjection(); m_image->m_d->isolatedRootNode = m_node; emit m_image->sigIsolatedModeChanged(); // the GUI uses our thread to do the color space conversion so we // need to emit this signal in multiple threads QVector jobs; m_image->m_d->notifyProjectionUpdatedInPatches(m_image->bounds(), jobs); this->runnableJobsInterface()->addRunnableJobs(jobs); m_image->invalidateAllFrames(); } private: KisNodeSP m_node; KisImageSP m_image; }; KisStrokeId id = startStroke(new StartIsolatedModeStroke(node, this)); endStroke(id); return true; } void KisImage::stopIsolatedMode() { if (!m_d->isolatedRootNode) return; struct StopIsolatedModeStroke : public KisRunnableBasedStrokeStrategy { StopIsolatedModeStroke(KisImageSP image) : KisRunnableBasedStrokeStrategy("stop-isolated-mode", kundo2_noi18n("stop-isolated-mode")), m_image(image) { this->enableJob(JOB_INIT); this->enableJob(JOB_DOSTROKE, true); setClearsRedoOnStart(false); } void initStrokeCallback() { if (!m_image->m_d->isolatedRootNode) return; //KisNodeSP oldRootNode = m_image->m_d->isolatedRootNode; m_image->m_d->isolatedRootNode = 0; emit m_image->sigIsolatedModeChanged(); m_image->invalidateAllFrames(); // the GUI uses our thread to do the color space conversion so we // need to emit this signal in multiple threads QVector jobs; m_image->m_d->notifyProjectionUpdatedInPatches(m_image->bounds(), jobs); this->runnableJobsInterface()->addRunnableJobs(jobs); // TODO: Substitute notifyProjectionUpdated() with this code // when update optimization is implemented // // QRect updateRect = bounds() | oldRootNode->extent(); // oldRootNode->setDirty(updateRect); } private: KisImageSP m_image; }; KisStrokeId id = startStroke(new StopIsolatedModeStroke(this)); endStroke(id); } KisNodeSP KisImage::isolatedModeRoot() const { return m_d->isolatedRootNode; } void KisImage::addJob(KisStrokeId id, KisStrokeJobData *data) { KisUpdateTimeMonitor::instance()->reportJobStarted(data); m_d->scheduler.addJob(id, data); } void KisImage::endStroke(KisStrokeId id) { m_d->scheduler.endStroke(id); } bool KisImage::cancelStroke(KisStrokeId id) { return m_d->scheduler.cancelStroke(id); } bool KisImage::KisImagePrivate::tryCancelCurrentStrokeAsync() { return scheduler.tryCancelCurrentStrokeAsync(); } void KisImage::requestUndoDuringStroke() { emit sigUndoDuringStrokeRequested(); } void KisImage::requestStrokeCancellation() { if (!m_d->tryCancelCurrentStrokeAsync()) { emit sigStrokeCancellationRequested(); } } UndoResult KisImage::tryUndoUnfinishedLod0Stroke() { return m_d->scheduler.tryUndoLastStrokeAsync(); } void KisImage::requestStrokeEnd() { emit sigStrokeEndRequested(); emit sigStrokeEndRequestedActiveNodeFiltered(); } void KisImage::requestStrokeEndActiveNode() { emit sigStrokeEndRequested(); } void KisImage::refreshGraph(KisNodeSP root) { refreshGraph(root, bounds(), bounds()); } void KisImage::refreshGraph(KisNodeSP root, const QRect &rc, const QRect &cropRect) { if (!root) root = m_d->rootLayer; m_d->animationInterface->notifyNodeChanged(root.data(), rc, true); m_d->scheduler.fullRefresh(root, rc, cropRect); } void KisImage::initialRefreshGraph() { /** * NOTE: Tricky part. We set crop rect to null, so the clones * will not rely on precalculated projections of their sources */ refreshGraphAsync(0, bounds(), QRect()); waitForDone(); } void KisImage::refreshGraphAsync(KisNodeSP root) { refreshGraphAsync(root, bounds(), bounds()); } void KisImage::refreshGraphAsync(KisNodeSP root, const QRect &rc) { refreshGraphAsync(root, rc, bounds()); } void KisImage::refreshGraphAsync(KisNodeSP root, const QRect &rc, const QRect &cropRect) { if (!root) root = m_d->rootLayer; m_d->animationInterface->notifyNodeChanged(root.data(), rc, true); m_d->scheduler.fullRefreshAsync(root, rc, cropRect); } void KisImage::requestProjectionUpdateNoFilthy(KisNodeSP pseudoFilthy, const QRect &rc, const QRect &cropRect) { KIS_ASSERT_RECOVER_RETURN(pseudoFilthy); m_d->animationInterface->notifyNodeChanged(pseudoFilthy.data(), rc, false); m_d->scheduler.updateProjectionNoFilthy(pseudoFilthy, rc, cropRect); } void KisImage::addSpontaneousJob(KisSpontaneousJob *spontaneousJob) { m_d->scheduler.addSpontaneousJob(spontaneousJob); } bool KisImage::hasUpdatesRunning() const { return m_d->scheduler.hasUpdatesRunning(); } void KisImage::setProjectionUpdatesFilter(KisProjectionUpdatesFilterSP filter) { // update filters are *not* recursive! KIS_ASSERT_RECOVER_NOOP(!filter || !m_d->projectionUpdatesFilter); m_d->projectionUpdatesFilter = filter; } KisProjectionUpdatesFilterSP KisImage::projectionUpdatesFilter() const { return m_d->projectionUpdatesFilter; } void KisImage::disableDirtyRequests() { setProjectionUpdatesFilter(KisProjectionUpdatesFilterSP(new KisDropAllProjectionUpdatesFilter())); } void KisImage::enableDirtyRequests() { setProjectionUpdatesFilter(KisProjectionUpdatesFilterSP()); } void KisImage::disableUIUpdates() { m_d->disableUIUpdateSignals.ref(); } void KisImage::notifyBatchUpdateStarted() { m_d->signalRouter.emitNotifyBatchUpdateStarted(); } void KisImage::notifyBatchUpdateEnded() { m_d->signalRouter.emitNotifyBatchUpdateEnded(); } void KisImage::notifyUIUpdateCompleted(const QRect &rc) { notifyProjectionUpdated(rc); } QVector KisImage::enableUIUpdates() { m_d->disableUIUpdateSignals.deref(); QRect rect; QVector postponedUpdates; while (m_d->savedDisabledUIUpdates.pop(rect)) { postponedUpdates.append(rect); } return postponedUpdates; } void KisImage::notifyProjectionUpdated(const QRect &rc) { KisUpdateTimeMonitor::instance()->reportUpdateFinished(rc); if (!m_d->disableUIUpdateSignals) { int lod = currentLevelOfDetail(); QRect dirtyRect = !lod ? rc : KisLodTransform::upscaledRect(rc, lod); if (dirtyRect.isEmpty()) return; emit sigImageUpdated(dirtyRect); } else { m_d->savedDisabledUIUpdates.push(rc); } } void KisImage::setWorkingThreadsLimit(int value) { m_d->scheduler.setThreadsLimit(value); } int KisImage::workingThreadsLimit() const { return m_d->scheduler.threadsLimit(); } void KisImage::notifySelectionChanged() { /** - * The selection is calculated asynchromously, so it is not + * The selection is calculated asynchronously, so it is not * handled by disableUIUpdates() and other special signals of * KisImageSignalRouter */ m_d->legacyUndoAdapter.emitSelectionChanged(); /** * Editing of selection masks doesn't necessary produce a * setDirty() call, so in the end of the stroke we need to request * direct update of the UI's cache. */ if (m_d->isolatedRootNode && dynamic_cast(m_d->isolatedRootNode.data())) { notifyProjectionUpdated(bounds()); } } void KisImage::requestProjectionUpdateImpl(KisNode *node, const QVector &rects, const QRect &cropRect) { if (rects.isEmpty()) return; m_d->scheduler.updateProjection(node, rects, cropRect); } void KisImage::requestProjectionUpdate(KisNode *node, const QVector &rects, bool resetAnimationCache) { if (m_d->projectionUpdatesFilter && m_d->projectionUpdatesFilter->filter(this, node, rects, resetAnimationCache)) { return; } if (resetAnimationCache) { m_d->animationInterface->notifyNodeChanged(node, rects, false); } /** * Here we use 'permitted' instead of 'active' intentively, * because the updates may come after the actual stroke has been * finished. And having some more updates for the stroke not * supporting the wrap-around mode will not make much harm. */ if (m_d->wrapAroundModePermitted) { QVector allSplitRects; const QRect boundRect = effectiveLodBounds(); Q_FOREACH (const QRect &rc, rects) { KisWrappedRect splitRect(rc, boundRect); allSplitRects.append(splitRect); } requestProjectionUpdateImpl(node, allSplitRects, boundRect); } else { requestProjectionUpdateImpl(node, rects, bounds()); } KisNodeGraphListener::requestProjectionUpdate(node, rects, resetAnimationCache); } void KisImage::invalidateFrames(const KisTimeRange &range, const QRect &rect) { m_d->animationInterface->invalidateFrames(range, rect); } void KisImage::requestTimeSwitch(int time) { m_d->animationInterface->requestTimeSwitchNonGUI(time); } KisNode *KisImage::graphOverlayNode() const { return m_d->overlaySelectionMask.data(); } QList KisImage::compositions() { return m_d->compositions; } void KisImage::addComposition(KisLayerCompositionSP composition) { m_d->compositions.append(composition); } void KisImage::removeComposition(KisLayerCompositionSP composition) { m_d->compositions.removeAll(composition); } bool checkMasksNeedConversion(KisNodeSP root, const QRect &bounds) { KisSelectionMask *mask = dynamic_cast(root.data()); if (mask && (!bounds.contains(mask->paintDevice()->exactBounds()) || mask->selection()->hasShapeSelection())) { return true; } KisNodeSP node = root->firstChild(); while (node) { if (checkMasksNeedConversion(node, bounds)) { return true; } node = node->nextSibling(); } return false; } void KisImage::setWrapAroundModePermitted(bool value) { if (m_d->wrapAroundModePermitted != value) { requestStrokeEnd(); } m_d->wrapAroundModePermitted = value; if (m_d->wrapAroundModePermitted && checkMasksNeedConversion(root(), bounds())) { KisProcessingApplicator applicator(this, root(), KisProcessingApplicator::RECURSIVE, KisImageSignalVector() << ModifiedSignal, kundo2_i18n("Crop Selections")); KisProcessingVisitorSP visitor = new KisCropSelectionsProcessingVisitor(bounds()); applicator.applyVisitor(visitor, KisStrokeJobData::CONCURRENT); applicator.end(); } } bool KisImage::wrapAroundModePermitted() const { return m_d->wrapAroundModePermitted; } bool KisImage::wrapAroundModeActive() const { return m_d->wrapAroundModePermitted && m_d->scheduler.wrapAroundModeSupported(); } void KisImage::setDesiredLevelOfDetail(int lod) { if (m_d->blockLevelOfDetail) { qWarning() << "WARNING: KisImage::setDesiredLevelOfDetail()" << "was called while LoD functionality was being blocked!"; return; } m_d->scheduler.setDesiredLevelOfDetail(lod); } int KisImage::currentLevelOfDetail() const { if (m_d->blockLevelOfDetail) { return 0; } return m_d->scheduler.currentLevelOfDetail(); } void KisImage::setLevelOfDetailBlocked(bool value) { KisImageBarrierLockerRaw l(this); if (value && !m_d->blockLevelOfDetail) { m_d->scheduler.setDesiredLevelOfDetail(0); } m_d->blockLevelOfDetail = value; } void KisImage::explicitRegenerateLevelOfDetail() { if (!m_d->blockLevelOfDetail) { m_d->scheduler.explicitRegenerateLevelOfDetail(); } } bool KisImage::levelOfDetailBlocked() const { return m_d->blockLevelOfDetail; } void KisImage::nodeCollapsedChanged(KisNode * node) { Q_UNUSED(node); emit sigNodeCollapsedChanged(); } KisImageAnimationInterface* KisImage::animationInterface() const { return m_d->animationInterface; } void KisImage::setProofingConfiguration(KisProofingConfigurationSP proofingConfig) { m_d->proofingConfig = proofingConfig; emit sigProofingConfigChanged(); } KisProofingConfigurationSP KisImage::proofingConfiguration() const { if (m_d->proofingConfig) { return m_d->proofingConfig; } return KisProofingConfigurationSP(); } QPointF KisImage::mirrorAxesCenter() const { return m_d->axesCenter; } void KisImage::setMirrorAxesCenter(const QPointF &value) const { m_d->axesCenter = value; } void KisImage::setAllowMasksOnRootNode(bool value) { m_d->allowMasksOnRootNode = value; } bool KisImage::allowMasksOnRootNode() const { return m_d->allowMasksOnRootNode; } diff --git a/libs/image/kis_image.h b/libs/image/kis_image.h index 12f78a29bb..042a2d1515 100644 --- a/libs/image/kis_image.h +++ b/libs/image/kis_image.h @@ -1,1189 +1,1189 @@ /* * Copyright (c) 2002 Patrick Julien * Copyright (c) 2007 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_IMAGE_H_ #define KIS_IMAGE_H_ #include #include #include #include #include #include #include #include "kis_types.h" #include "kis_shared.h" #include "kis_node_graph_listener.h" #include "kis_node_facade.h" #include "kis_image_interfaces.h" #include "kis_strokes_queue_undo_result.h" #include class KoColorSpace; class KoColor; class KisCompositeProgressProxy; class KisUndoStore; class KisUndoAdapter; class KisImageSignalRouter; class KisPostExecutionUndoAdapter; class KisFilterStrategy; class KoColorProfile; class KisLayerComposition; class KisSpontaneousJob; class KisImageAnimationInterface; class KUndo2MagicString; class KisProofingConfiguration; class KisPaintDevice; namespace KisMetaData { class MergeStrategy; } /** * This is the image class, it contains a tree of KisLayer stack and * meta information about the image. And it also provides some * functions to manipulate the whole image. */ class KRITAIMAGE_EXPORT KisImage : public QObject, public KisStrokesFacade, public KisStrokeUndoFacade, public KisUpdatesFacade, public KisProjectionUpdateListener, public KisNodeFacade, public KisNodeGraphListener, public KisShared { Q_OBJECT public: /// @p colorSpace can be null. In that case, it will be initialised to a default color space. KisImage(KisUndoStore *undoStore, qint32 width, qint32 height, const KoColorSpace *colorSpace, const QString& name); ~KisImage() override; static KisImageSP fromQImage(const QImage &image, KisUndoStore *undoStore); public: // KisNodeGraphListener implementation void aboutToAddANode(KisNode *parent, int index) override; void nodeHasBeenAdded(KisNode *parent, int index) override; void aboutToRemoveANode(KisNode *parent, int index) override; void nodeChanged(KisNode * node) override; void nodeCollapsedChanged(KisNode *node) override; void invalidateAllFrames() override; void notifySelectionChanged() override; void requestProjectionUpdate(KisNode *node, const QVector &rects, bool resetAnimationCache) override; void invalidateFrames(const KisTimeRange &range, const QRect &rect) override; void requestTimeSwitch(int time) override; KisNode* graphOverlayNode() const override; public: // KisProjectionUpdateListener implementation void notifyProjectionUpdated(const QRect &rc) override; public: /** * Set the number of threads used by the image's working threads */ void setWorkingThreadsLimit(int value); /** * Return the number of threads available to the image's working threads */ int workingThreadsLimit() const; /** * Makes a copy of the image with all the layers. If possible, shallow * copies of the layers are made. * * \p exactCopy shows if the copied image should look *exactly* the same as * the other one (according to it's .kra xml representation). It means that * the layers will have the same UUID keys and, therefore, you are not * expected to use the copied image anywhere except for saving. Don't use * this option if you plan to work with the copied image later. */ KisImage *clone(bool exactCopy = false); void copyFromImage(const KisImage &rhs); private: // must specify exactly one from CONSTRUCT or REPLACE. enum CopyPolicy { CONSTRUCT = 1, ///< we are copy-constructing a new KisImage REPLACE = 2, ///< we are replacing the current KisImage with another EXACT_COPY = 4, /// we need an exact copy of the original image }; void copyFromImageImpl(const KisImage &rhs, int policy); public: /** * Render the projection onto a QImage. */ QImage convertToQImage(qint32 x1, qint32 y1, qint32 width, qint32 height, const KoColorProfile * profile); /** * Render the projection onto a QImage. * (this is an overloaded function) */ QImage convertToQImage(QRect imageRect, const KoColorProfile * profile); /** * Render a thumbnail of the projection onto a QImage. */ QImage convertToQImage(const QSize& scaledImageSize, const KoColorProfile *profile); /** * [low-level] Lock the image without waiting for all the internal job queues are processed * * WARNING: Don't use it unless you really know what you are doing! Use barrierLock() instead! * * Waits for all the **currently running** internal jobs to complete and locks the image * for writing. Please note that this function does **not** wait for all the internal * queues to process, so there might be some non-finished actions pending. It means that * you just postpone these actions until you unlock() the image back. Until then, then image * might easily be frozen in some inconsistent state. * * The only sane usage for this function is to lock the image for **emergency** * processing, when some internal action or scheduler got hung up, and you just want * to fetch some data from the image without races. * * In all other cases, please use barrierLock() instead! */ void lock(); /** * Unlocks the image and starts/resumes all the pending internal jobs. If the image * has been locked for a non-readOnly access, then all the internal caches of the image * (e.g. lod-planes) are reset and regeneration jobs are scheduled. */ void unlock(); /** * @return return true if the image is in a locked state, i.e. all the internal * jobs are blocked from execution by calling wither lock() or barrierLock(). * * When the image is locked, the user can do some modifications to the image * contents safely without a perspective having race conditions with internal * image jobs. */ bool locked() const; /** * Sets the mask (it must be a part of the node hierarchy already) to be paited on * the top of all layers. This method does all the locking and syncing for you. It * is executed asynchronously. */ void setOverlaySelectionMask(KisSelectionMaskSP mask); /** * \see setOverlaySelectionMask */ KisSelectionMaskSP overlaySelectionMask() const; /** * \see setOverlaySelectionMask */ bool hasOverlaySelectionMask() const; /** * @return the global selection object or 0 if there is none. The * global selection is always read-write. */ KisSelectionSP globalSelection() const; /** * Retrieve the next automatic layername (XXX: fix to add option to return Mask X) */ QString nextLayerName(const QString &baseName = "") const; /** * Set the automatic layer name counter one back. */ void rollBackLayerName(); /** * @brief start asynchronous operation on resizing the image * * The method will resize the image to fit the new size without * dropping any pixel data. The GUI will get correct * notification with old and new sizes, so it adjust canvas origin * accordingly and avoid jumping of the canvas on screen * * @param newRect the rectangle of the image which will be visible * after operation is completed * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the image having new size * right after this call. */ void resizeImage(const QRect& newRect); /** * @brief start asynchronous operation on cropping the image * * The method will **drop** all the image data outside \p newRect * and resize the image to fit the new size. The GUI will get correct * notification with old and new sizes, so it adjust canvas origin * accordingly and avoid jumping of the canvas on screen * * @param newRect the rectangle of the image which will be cut-out * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the image having new size * right after this call. */ void cropImage(const QRect& newRect); /** * @brief start asynchronous operation on cropping a subtree of nodes starting at \p node * * The method will **drop** all the layer data outside \p newRect. Neither * image nor a layer will be moved anywhere * * @param node node to crop * @param newRect the rectangle of the layer which will be cut-out * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the image having new size * right after this call. */ void cropNode(KisNodeSP node, const QRect& newRect); /** * @brief start asynchronous operation on scaling the image * @param size new image size in pixels * @param xres new image x-resolution pixels-per-pt * @param yres new image y-resolution pixels-per-pt * @param filterStrategy filtering strategy * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the image having new size * right after this call. */ void scaleImage(const QSize &size, qreal xres, qreal yres, KisFilterStrategy *filterStrategy); /** * @brief start asynchronous operation on scaling a subtree of nodes starting at \p node * @param node node to scale * @param center the center of the scaling * @param scaleX x-scale coefficient to be applied to the node * @param scaleY y-scale coefficient to be applied to the node * @param filterStrategy filtering strategy * @param selection the selection we based on * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the image having new size * right after this call. */ void scaleNode(KisNodeSP node, const QPointF ¢er, qreal scaleX, qreal scaleY, KisFilterStrategy *filterStrategy, KisSelectionSP selection); /** * @brief start asynchronous operation on rotating the image * * The image is resized to fit the rotated rectangle * * @param radians rotation angle in radians * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the operation being completed * right after the call */ void rotateImage(double radians); /** * @brief start asynchronous operation on rotating a subtree of nodes starting at \p node * * The image is not resized! * * @param node the root of the subtree to rotate * @param radians rotation angle in radians * @param selection the selection we based on * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the operation being completed * right after the call */ void rotateNode(KisNodeSP node, double radians, KisSelectionSP selection); /** * @brief start asynchronous operation on shearing the image * * The image is resized to fit the sheared polygon * * @p angleX, @p angleY are given in degrees. * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the operation being completed * right after the call */ void shear(double angleX, double angleY); /** * @brief start asynchronous operation on shearing a subtree of nodes starting at \p node * * The image is not resized! * * @param node the root of the subtree to rotate * @param angleX x-shear given in degrees. * @param angleY y-shear given in degrees. * @param selection the selection we based on * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the operation being completed * right after the call */ void shearNode(KisNodeSP node, double angleX, double angleY, KisSelectionSP selection); /** * Convert the image and all its layers to the dstColorSpace */ void convertImageColorSpace(const KoColorSpace *dstColorSpace, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags); /** * Set the color space of the projection (and the root layer) * to dstColorSpace. No conversion is done for other layers, * their colorspace can differ. * @note No conversion is done, only regeneration, so no rendering * intent needed */ void convertProjectionColorSpace(const KoColorSpace *dstColorSpace); // Get the profile associated with this image const KoColorProfile * profile() const; /** * Set the profile of the image to the new profile and do the same for * all layers that have the same colorspace and profile of the image. * It doesn't do any pixel conversion. * * This is essential if you have loaded an image that didn't * have an embedded profile to which you want to attach the right profile. * * This does not create an undo action; only call it when creating or * loading an image. * * @returns false if the profile could not be assigned */ bool assignImageProfile(const KoColorProfile *profile); /** * Returns the current undo adapter. You can add new commands to the * undo stack using the adapter. This adapter is used for a backward * compatibility for old commands created before strokes. It blocks * all the porcessing at the scheduler, waits until it's finished * and executes commands exclusively. */ KisUndoAdapter* undoAdapter() const; /** * This adapter is used by the strokes system. The commands are added * to it *after* redo() is done (in the scheduler context). They are * wrapped into a special command and added to the undo stack. redo() * in not called. */ KisPostExecutionUndoAdapter* postExecutionUndoAdapter() const override; /** * Return the lastly executed LoD0 command. It is effectively the same * as to call undoAdapter()->presentCommand(); */ const KUndo2Command* lastExecutedCommand() const override; /** * Replace current undo store with the new one. The old store * will be deleted. * This method is used by KisDocument for dropping all the commands * during file loading. */ void setUndoStore(KisUndoStore *undoStore); /** * Return current undo store of the image */ KisUndoStore* undoStore(); /** * Tell the image it's modified; this emits the sigImageModified * signal. This happens when the image needs to be saved */ void setModified(); /** * The default colorspace of this image: new layers will have this * colorspace and the projection will have this colorspace. */ const KoColorSpace * colorSpace() const; /** * X resolution in pixels per pt */ double xRes() const; /** * Y resolution in pixels per pt */ double yRes() const; /** * Set the resolution in pixels per pt. */ void setResolution(double xres, double yres); /** * Convert a document coordinate to a pixel coordinate. * * @param documentCoord PostScript Pt coordinate to convert. */ QPointF documentToPixel(const QPointF &documentCoord) const; /** * Convert a document coordinate to an integer pixel coordinate rounded down. * * @param documentCoord PostScript Pt coordinate to convert. */ QPoint documentToImagePixelFloored(const QPointF &documentCoord) const; /** * Convert a document rectangle to a pixel rectangle. * * @param documentRect PostScript Pt rectangle to convert. */ QRectF documentToPixel(const QRectF &documentRect) const; /** * Convert a pixel coordinate to a document coordinate. * * @param pixelCoord pixel coordinate to convert. */ QPointF pixelToDocument(const QPointF &pixelCoord) const; /** * Convert an integer pixel coordinate to a document coordinate. * The document coordinate is at the centre of the pixel. * * @param pixelCoord pixel coordinate to convert. */ QPointF pixelToDocument(const QPoint &pixelCoord) const; /** * Convert a document rectangle to an integer pixel rectangle. * * @param pixelCoord pixel coordinate to convert. */ QRectF pixelToDocument(const QRectF &pixelCoord) const; /** * Return the width of the image */ qint32 width() const; /** * Return the height of the image */ qint32 height() const; /** * Return the size of the image */ QSize size() const { return QSize(width(), height()); } /** * @return the root node of the image node graph */ KisGroupLayerSP rootLayer() const; /** * Return the projection; that is, the complete, composited * representation of this image. */ KisPaintDeviceSP projection() const; /** * Return the number of layers (not other nodes) that are in this * image. */ qint32 nlayers() const; /** * Return the number of layers (not other node types) that are in * this image and that are hidden. */ qint32 nHiddenLayers() const; /** * Merge all visible layers and discard hidden ones. */ void flatten(KisNodeSP activeNode); /** * Merge the specified layer with the layer * below this layer, remove the specified layer. */ void mergeDown(KisLayerSP l, const KisMetaData::MergeStrategy* strategy); /** * flatten the layer: that is, the projection becomes the layer * and all subnodes are removed. If this is not a paint layer, it will morph * into a paint layer. */ void flattenLayer(KisLayerSP layer); /** * Merges layers in \p mergedLayers and creates a new layer above * \p putAfter */ void mergeMultipleLayers(QList mergedLayers, KisNodeSP putAfter); /// @return the exact bounds of the image in pixel coordinates. QRect bounds() const override; /** * Returns the actual bounds of the image, taking LevelOfDetail * into account. This value is used as a bounds() value of * KisDefaultBounds object. */ QRect effectiveLodBounds() const; /// use if the layers have changed _completely_ (eg. when flattening) void notifyLayersChanged(); /** * Sets the default color of the root layer projection. All the layers * will be merged on top of this very color */ void setDefaultProjectionColor(const KoColor &color); /** * \see setDefaultProjectionColor() */ KoColor defaultProjectionColor() const; void setRootLayer(KisGroupLayerSP rootLayer); /** * Add an annotation for this image. This can be anything: Gamma, EXIF, etc. * Note that the "icc" annotation is reserved for the color strategies. * If the annotation already exists, overwrite it with this one. */ void addAnnotation(KisAnnotationSP annotation); /** get the annotation with the given type, can return 0 */ KisAnnotationSP annotation(const QString& type); /** delete the annotation, if the image contains it */ void removeAnnotation(const QString& type); /** * Start of an iteration over the annotations of this image (including the ICC Profile) */ vKisAnnotationSP_it beginAnnotations(); /** end of an iteration over the annotations of this image */ vKisAnnotationSP_it endAnnotations(); /** * Called before the image is deleted and sends the sigAboutToBeDeleted signal */ void notifyAboutToBeDeleted(); KisImageSignalRouter* signalRouter(); /** * Returns whether we can reselect current global selection * * \see reselectGlobalSelection() */ bool canReselectGlobalSelection(); /** * Returns the layer compositions for the image */ QList compositions(); /** * Adds a new layer composition, will be saved with the image */ void addComposition(KisLayerCompositionSP composition); /** * Remove the layer compostion */ void removeComposition(KisLayerCompositionSP composition); /** * Permit or deny the wrap-around mode for all the paint devices * of the image. Note that permitting the wraparound mode will not * necessarily activate it right now. To be activated the wrap * around mode should be 1) permitted; 2) supported by the * currently running stroke. */ void setWrapAroundModePermitted(bool value); /** * \return whether the wrap-around mode is permitted for this * image. If the wrap around mode is permitted and the * currently running stroke supports it, the mode will be * activated for all paint devices of the image. * * \see setWrapAroundMode */ bool wrapAroundModePermitted() const; /** * \return whether the wraparound mode is activated for all the * devices of the image. The mode is activated when both * factors are true: the user permitted it and the stroke * supports it */ bool wrapAroundModeActive() const; /** * \return current level of detail which is used when processing the image. * Current working zoom = 2 ^ (- currentLevelOfDetail()). Default value is * null, which means we work on the original image. */ int currentLevelOfDetail() const; /** * Notify KisImage which level of detail should be used in the * lod-mode. Setting the mode does not guarantee the LOD to be * used. It will be activated only when the stokes supports it. */ void setDesiredLevelOfDetail(int lod); /** * Relative position of the mirror axis center * 0,0 - topleft corner of the image * 1,1 - bottomright corner of the image */ QPointF mirrorAxesCenter() const; /** * Sets the relative position of the axes center * \see mirrorAxesCenter() for details */ void setMirrorAxesCenter(const QPointF &value) const; /** * Configure the image to allow masks on the root not (as reported by * root()->allowAsChild()). By default it is not allowed (because it * looks weird from GUI point of view) */ void setAllowMasksOnRootNode(bool value); /** * \see setAllowMasksOnRootNode() */ bool allowMasksOnRootNode() const; public Q_SLOTS: /** * Explicitly start regeneration of LoD planes of all the devices * in the image. This call should be performed when the user is idle, * just to make the quality of image updates better. */ void explicitRegenerateLevelOfDetail(); public: /** * Blocks usage of level of detail functionality. After this method * has been called, no new strokes will use LoD. */ void setLevelOfDetailBlocked(bool value); /** * \see setLevelOfDetailBlocked() */ bool levelOfDetailBlocked() const; KisImageAnimationInterface *animationInterface() const; /** * @brief setProofingConfiguration, this sets the image's proofing configuration, and signals * the proofingConfiguration has changed. * @param proofingConfig - the kis proofing config that will be used instead. */ void setProofingConfiguration(KisProofingConfigurationSP proofingConfig); /** * @brief proofingConfiguration * @return the proofing configuration of the image. */ KisProofingConfigurationSP proofingConfiguration() const; public Q_SLOTS: bool startIsolatedMode(KisNodeSP node); void stopIsolatedMode(); public: KisNodeSP isolatedModeRoot() const; Q_SIGNALS: /** * Emitted whenever an action has caused the image to be * recomposited. Parameter is the rect that has been recomposited. */ void sigImageUpdated(const QRect &); /** Emitted whenever the image has been modified, so that it doesn't match with the version saved on disk. */ void sigImageModified(); /** * The signal is emitted when the size of the image is changed. * \p oldStillPoint and \p newStillPoint give the receiver the * hint about how the new and old rect of the image correspond to * each other. They specify the point of the image around which * the conversion was done. This point will stay still on the * user's screen. That is the \p newStillPoint of the new image * will be painted at the same screen position, where \p * oldStillPoint of the old image was painted. * * \param oldStillPoint is a still point represented in *old* * image coordinates * * \param newStillPoint is a still point represented in *new* * image coordinates */ void sigSizeChanged(const QPointF &oldStillPoint, const QPointF &newStillPoint); void sigProfileChanged(const KoColorProfile * profile); void sigColorSpaceChanged(const KoColorSpace* cs); void sigResolutionChanged(double xRes, double yRes); void sigRequestNodeReselection(KisNodeSP activeNode, const KisNodeList &selectedNodes); /** * Inform the model that a node was changed */ void sigNodeChanged(KisNodeSP node); /** * Inform that the image is going to be deleted */ void sigAboutToBeDeleted(); /** * The signal is emitted right after a node has been connected * to the graph of the nodes. * * WARNING: you must not request any graph-related information * about the node being run in a not-scheduler thread. If you need * information about the parent/siblings of the node connect * with Qt::DirectConnection, get needed information and then * emit another Qt::AutoConnection signal to pass this information * to your thread. See details of the implementation * in KisDummiesfacadeBase. */ void sigNodeAddedAsync(KisNodeSP node); /** * This signal is emitted right before a node is going to removed * from the graph of the nodes. * * WARNING: you must not request any graph-related information * about the node being run in a not-scheduler thread. * * \see comment in sigNodeAddedAsync() */ void sigRemoveNodeAsync(KisNodeSP node); /** * Emitted when the root node of the image has changed. * It happens, e.g. when we flatten the image. When * this happens the receiver should reload information * about the image */ void sigLayersChangedAsync(); /** * Emitted when the UI has requested the undo of the last stroke's * operation. The point is, we cannot deal with the internals of * the stroke without its creator knowing about it (which most * probably cause a crash), so we just forward this request from * the UI to the creator of the stroke. * * If your tool supports undoing part of its work, just listen to * this signal and undo when it comes */ void sigUndoDuringStrokeRequested(); /** * Emitted when the UI has requested the cancellation of * the stroke. The point is, we cannot cancel the stroke * without its creator knowing about it (which most probably * cause a crash), so we just forward this request from the UI * to the creator of the stroke. * * If your tool supports cancelling of its work in the middle * of operation, just listen to this signal and cancel * the stroke when it comes */ void sigStrokeCancellationRequested(); /** * Emitted when the image decides that the stroke should better * be ended. The point is, we cannot just end the stroke * without its creator knowing about it (which most probably * cause a crash), so we just forward this request from the UI * to the creator of the stroke. * * If your tool supports long strokes that may involve multiple * mouse actions in one stroke, just listen to this signal and * end the stroke when it comes. */ void sigStrokeEndRequested(); /** * Same as sigStrokeEndRequested() but is not emitted when the active node * is changed. */ void sigStrokeEndRequestedActiveNodeFiltered(); /** * Emitted when the isolated mode status has changed. * * Can be used by the receivers to catch a fact of forcefully * stopping the isolated mode by the image when some complex * action was requested */ void sigIsolatedModeChanged(); /** * Emitted when one or more nodes changed the collapsed state * */ void sigNodeCollapsedChanged(); /** * Emitted when the proofing configuration of the image is being changed. * */ void sigProofingConfigChanged(); /** * Internal signal for asynchronously requesting isolated mode to stop. Don't use it * outside KisImage, use sigIsolatedModeChanged() instead. */ void sigInternalStopIsolatedModeRequested(); public Q_SLOTS: KisCompositeProgressProxy* compositeProgressProxy(); bool isIdle(bool allowLocked = false); /** * @brief Wait until all the queued background jobs are completed and lock the image. * * KisImage object has a local scheduler that executes long-running image * rendering/modifying jobs (we call them "strokes") in a background. Basically, * one should either access the image from the scope of such jobs (strokes) or * just lock the image before using. * * Calling barrierLock() will wait until all the queued operations are finished * and lock the image, so you can start accessing it in a safe way. * * @p readOnly tells the image if the caller is going to modify the image during * holding the lock. Locking with non-readOnly access will reset all * the internal caches of the image (lod-planes) when the lock status * will be lifted. */ void barrierLock(bool readOnly = false); /** * @brief Tries to lock the image without waiting for the jobs to finish * * Same as barrierLock(), but doesn't block execution of the calling thread * until all the background jobs are finished. Instead, in case of presence of * unfinished jobs in the queue, it just returns false * * @return whether the lock has been acquired * @see barrierLock */ bool tryBarrierLock(bool readOnly = false); /** * Wait for all the internal image jobs to complete and return without locking * the image. This function is handly for tests or other synchronous actions, * when one needs to wait for the result of his actions. */ void waitForDone(); KisStrokeId startStroke(KisStrokeStrategy *strokeStrategy) override; void addJob(KisStrokeId id, KisStrokeJobData *data) override; void endStroke(KisStrokeId id) override; bool cancelStroke(KisStrokeId id) override; /** * @brief blockUpdates block updating the image projection */ void blockUpdates() override; /** * @brief unblockUpdates unblock updating the image project. This * only restarts the scheduler and does not schedule a full refresh. */ void unblockUpdates() override; /** * Disables notification of the UI about the changes in the image. * This feature is used by KisProcessingApplicator. It is needed * when we change the size of the image. In this case, the whole * image will be reloaded into UI by sigSizeChanged(), so there is * no need to inform the UI about individual dirty rects. * - * The last call to enableUIUpdates() will return the list of udpates + * The last call to enableUIUpdates() will return the list of updates * that were requested while they were blocked. */ void disableUIUpdates() override; /** * Notify GUI about a bunch of updates planned. GUI is expected to wait * until all the updates are completed, and render them on screen only * in the very and of the batch. */ void notifyBatchUpdateStarted() override; /** * Notify GUI that batch update has been completed. Now GUI can start * showing all of them on screen. */ void notifyBatchUpdateEnded() override; /** * Notify GUI that rect \p rc is now prepared in the image and * GUI can read data from it. * * WARNING: GUI will read the data right in the handler of this * signal, so exclusive access to the area must be guaranteed * by the caller. */ void notifyUIUpdateCompleted(const QRect &rc) override; /** * \see disableUIUpdates */ QVector enableUIUpdates() override; /** * Disables the processing of all the setDirty() requests that * come to the image. The incoming requests are effectively * *dropped*. * * This feature is used by KisProcessingApplicator. For many cases * it provides its own updates interface, which recalculates the * whole subtree of nodes. But while we change any particular * node, it can ask for an update itself. This method is a way of * blocking such intermediate (and excessive) requests. * * NOTE: this is a convenience function for setProjectionUpdatesFilter() * that installs a predefined filter that eats everything. Please * note that these calls are *not* recursive */ void disableDirtyRequests() override; /** * \see disableDirtyRequests() */ void enableDirtyRequests() override; /** * Installs a filter object that will filter all the incoming projection update * requests. If the filter return true, the incoming update is dropped. * * NOTE: you cannot set filters recursively! */ void setProjectionUpdatesFilter(KisProjectionUpdatesFilterSP filter) override; /** * \see setProjectionUpdatesFilter() */ KisProjectionUpdatesFilterSP projectionUpdatesFilter() const override; void refreshGraphAsync(KisNodeSP root = KisNodeSP()) override; void refreshGraphAsync(KisNodeSP root, const QRect &rc) override; void refreshGraphAsync(KisNodeSP root, const QRect &rc, const QRect &cropRect) override; /** * Triggers synchronous recomposition of the projection */ void refreshGraph(KisNodeSP root = KisNodeSP()); void refreshGraph(KisNodeSP root, const QRect& rc, const QRect &cropRect); void initialRefreshGraph(); /** * Initiate a stack regeneration skipping the recalculation of the * filthy node's projection. * * Works exactly as pseudoFilthy->setDirty() with the only * exception that pseudoFilthy::updateProjection() will not be * called. That is used by KisRecalculateTransformMaskJob to avoid * cyclic dependencies. */ void requestProjectionUpdateNoFilthy(KisNodeSP pseudoFilthy, const QRect &rc, const QRect &cropRect); /** * Adds a spontaneous job to the updates queue. * * A spontaneous job may do some trivial tasks in the background, * like updating the outline of selection or purging unused tiles * from the existing paint devices. */ void addSpontaneousJob(KisSpontaneousJob *spontaneousJob); /** * \return true if there are some updates in the updates queue * Please note, that is doesn't guarantee that there are no updates * running in in the updater context at the very moment. To guarantee that * there are no updates left at all, please use barrier jobs instead. */ bool hasUpdatesRunning() const override; /** * This method is called by the UI (*not* by the creator of the * stroke) when it thinks the current stroke should undo its last * action, for example, when the user presses Ctrl+Z while some * stroke is active. * * If the creator of the stroke supports undoing of intermediate * actions, it will be notified about this request and can undo * its last action. */ void requestUndoDuringStroke(); /** * This method is called by the UI (*not* by the creator of the * stroke) when it thinks current stroke should be cancelled. If * there is a running stroke that has already been detached from * its creator (ended or cancelled), it will be forcefully * cancelled and reverted. If there is an open stroke present, and * if its creator supports cancelling, it will be notified about * the request and the stroke will be cancelled */ void requestStrokeCancellation(); /** * This method requests the last stroke executed on the image to become undone. * If the stroke is not ended, or if all the Lod0 strokes are completed, the method * returns UNDO_FAIL. If the last Lod0 is going to be finished soon, then UNDO_WAIT * is returned and the caller should just wait for its completion and call global undo * instead. UNDO_OK means one unfinished stroke has been undone. */ UndoResult tryUndoUnfinishedLod0Stroke(); /** * This method is called when image or some other part of Krita * (*not* the creator of the stroke) decides that the stroke * should be ended. If the creator of the stroke supports it, it * will be notified and the stroke will be cancelled */ void requestStrokeEnd(); /** * Same as requestStrokeEnd() but is called by view manager when * the current node is changed. Use to distinguish * sigStrokeEndRequested() and * sigStrokeEndRequestedActiveNodeFiltered() which are used by * KisNodeJugglerCompressed */ void requestStrokeEndActiveNode(); private: KisImage(const KisImage& rhs, KisUndoStore *undoStore, bool exactCopy); KisImage& operator=(const KisImage& rhs); void emitSizeChanged(); void resizeImageImpl(const QRect& newRect, bool cropLayers); void rotateImpl(const KUndo2MagicString &actionName, KisNodeSP rootNode, double radians, bool resizeImage, KisSelectionSP selection); void shearImpl(const KUndo2MagicString &actionName, KisNodeSP rootNode, bool resizeImage, double angleX, double angleY, KisSelectionSP selection); void safeRemoveTwoNodes(KisNodeSP node1, KisNodeSP node2); void refreshHiddenArea(KisNodeSP rootNode, const QRect &preparedArea); void requestProjectionUpdateImpl(KisNode *node, const QVector &rects, const QRect &cropRect); friend class KisImageResizeCommand; void setSize(const QSize& size); friend class KisImageSetProjectionColorSpaceCommand; void setProjectionColorSpace(const KoColorSpace * colorSpace); friend class KisDeselectGlobalSelectionCommand; friend class KisReselectGlobalSelectionCommand; friend class KisSetGlobalSelectionCommand; friend class KisImageTest; friend class Document; // For libkis /** * Replaces the current global selection with globalSelection. If * \p globalSelection is empty, removes the selection object, so that * \ref globalSelection() will return 0 after that. */ void setGlobalSelection(KisSelectionSP globalSelection); /** * Deselects current global selection. * \ref globalSelection() will return 0 after that. */ void deselectGlobalSelection(); /** * Reselects current deselected selection * * \see deselectGlobalSelection() */ void reselectGlobalSelection(); private: class KisImagePrivate; KisImagePrivate * m_d; }; #endif // KIS_IMAGE_H_ diff --git a/libs/image/kis_node.h b/libs/image/kis_node.h index 07ca1fc29f..83083ca9d4 100644 --- a/libs/image/kis_node.h +++ b/libs/image/kis_node.h @@ -1,434 +1,434 @@ /* * Copyright (c) 2007 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_NODE_H #define _KIS_NODE_H #include "kis_types.h" #include "kis_base_node.h" #include "kritaimage_export.h" #include class QRect; class QStringList; class KoProperties; class KisNodeVisitor; class KisNodeGraphListener; class KisNodeProgressProxy; class KisBusyProgressIndicator; class KisAbstractProjectionPlane; class KisProjectionLeaf; class KisKeyframeChannel; class KisTimeRange; class KisUndoAdapter; /** * A KisNode is a KisBaseNode that knows about its direct peers, parent * and children and whether it can have children. * * THREAD-SAFETY: All const methods of this class and setDirty calls * are considered to be thread-safe(!). All the others * especially add(), remove() and setParent() must be * protected externally. * * NOTE: your subclasses must have the Q_OBJECT declaration, even if * you do not define new signals or slots. */ class KRITAIMAGE_EXPORT KisNode : public KisBaseNode { friend class KisFilterMaskTest; Q_OBJECT public: /** * The struct describing the position of the node * against the filthy node. * NOTE: please change KisBaseRectsWalker::getPositionToFilthy * when changing this struct */ enum PositionToFilthy { N_ABOVE_FILTHY = 0x08, N_FILTHY_PROJECTION = 0x20, N_FILTHY = 0x40, N_BELOW_FILTHY = 0x80 }; /** * Create an empty node without a parent. */ KisNode(KisImageWSP image); /** * Create a copy of this node. The copy will not have a parent * node. */ KisNode(const KisNode & rhs); /** * Delete this node */ ~KisNode() override; virtual KisNodeSP clone() const = 0; bool accept(KisNodeVisitor &v) override; void accept(KisProcessingVisitor &visitor, KisUndoAdapter *undoAdapter) override; /** * Re-implement this method to add constraints for the * subclasses that can be added as children to this node * * @return false if the given node is not allowed as a child to this node */ virtual bool allowAsChild(KisNodeSP) const = 0; /** * Set the entire node extent dirty; this percolates up to parent * nodes all the way to the root node. By default this is the * empty rect (through KisBaseNode::extent()) */ virtual void setDirty(); /** * Add the given rect to the set of dirty rects for this node; * this percolates up to parent nodes all the way to the root * node. */ void setDirty(const QRect & rect); /** * Add the given rects to the set of dirty rects for this node; * this percolates up to parent nodes all the way to the root * node. */ virtual void setDirty(const QVector &rects); /** * Add the given region to the set of dirty rects for this node; * this percolates up to parent nodes all the way to the root * node, if propagate is true; */ void setDirty(const QRegion ®ion); /** * Convenience override of multirect version of setDirtyDontResetAnimationCache() * * @see setDirtyDontResetAnimationCache(const QVector &rects) */ void setDirtyDontResetAnimationCache(); /** * Convenience override of multirect version of setDirtyDontResetAnimationCache() * * @see setDirtyDontResetAnimationCache(const QVector &rects) */ void setDirtyDontResetAnimationCache(const QRect &rect); /** * @brief setDirtyDontResetAnimationCache does almost the same thing as usual * setDirty() call, but doesn't reset the animation cache (since onlion skins are * not used when rendering animation. */ void setDirtyDontResetAnimationCache(const QVector &rects); /** * Informs that the frames in the given range are no longer valid * and need to be recached. * @param range frames to invalidate */ void invalidateFrames(const KisTimeRange &range, const QRect &rect); /** * Informs that the current world time should be changed. * Might be caused by e.g. undo operation */ void requestTimeSwitch(int time); /** * \return a pointer to a KisAbstractProjectionPlane interface of * the node. This interface is used by the image merging * framework to get information and to blending for the * layer. * * Please note the difference between need/change/accessRect and * the projectionPlane() interface. The former one gives * information about internal composition of the layer, and the * latter one about the total composition, including layer styles, * pass-through blending and etc. */ virtual KisAbstractProjectionPlaneSP projectionPlane() const; /** * Synchronizes LoD caches of the node with the current state of it. * The current level of detail is fetched from the image pointed by * default bounds object */ virtual void syncLodCache(); virtual KisPaintDeviceList getLodCapableDevices() const; /** * The rendering of the image may not always happen in the order - * of the main graph. Pass-through nodes ake some subgraphs + * of the main graph. Pass-through nodes make some subgraphs * linear, so it the order of rendering change. projectionLeaf() * is a special interface of KisNode that represents "a graph for * projection rendering". Therefore the nodes in projectionLeaf() * graph may have a different order the main one. */ virtual KisProjectionLeafSP projectionLeaf() const; void setImage(KisImageWSP image) override; protected: /** * \return internal changeRect() of the node. Do not mix with \see * projectionPlane() * * Some filters will cause a change of pixels those are outside * a requested rect. E.g. we change a rect of 2x2, then we want to * apply a convolution filter with kernel 4x4 (changeRect is * (2+2*3)x(2+2*3)=8x8) to that area. The rect that should be updated - * on the layer will be exaclty 8x8. More than that the needRect for + * on the layer will be exactly 8x8. More than that the needRect for * that update will be 14x14. See \ref needeRect. */ virtual QRect changeRect(const QRect &rect, PositionToFilthy pos = N_FILTHY) const; /** * \return internal needRect() of the node. Do not mix with \see * projectionPlane() * * Some filters need pixels outside the current processing rect to * compute the new value (for instance, convolution filters) * See \ref changeRect * See \ref accessRect */ virtual QRect needRect(const QRect &rect, PositionToFilthy pos = N_FILTHY) const; /** * \return internal accessRect() of the node. Do not mix with \see * projectionPlane() * * Shows the area of image, that may be accessed during accessing * the node. * * Example. You have a layer that needs to prepare some rect on a * projection, say expectedRect. To perform this, the projection * of all the layers below of the size needRect(expectedRect) * should be calculated by the merger beforehand and the layer * will access some other area of image inside the rect * accessRect(expectedRect) during updateProjection call. * * This knowledge about real access rect of a node is used by the * scheduler to avoid collisions between two multithreaded updaters * and so avoid flickering of the image. * * Currently, this method has nondefault value for shifted clone * layers only. */ virtual QRect accessRect(const QRect &rect, PositionToFilthy pos = N_FILTHY) const; /** * Called each time direct child nodes are added or removed under this * node as parent. This does not track changes inside the child nodes * or the child nodes' properties. */ virtual void childNodeChanged(KisNodeSP changedChildNode); public: // Graph methods /** * @return the graph sequence number calculated by the associated * graph listener. You can use it for checking for changes in the * graph. */ int graphSequenceNumber() const; /** * @return the graph listener this node belongs to. 0 if the node * does not belong to a grap listener. */ KisNodeGraphListener * graphListener() const; /** * Set the graph listener for this node. The graphlistener will be * informed before and after the list of child nodes has changed. */ void setGraphListener(KisNodeGraphListener * graphListener); /** * Returns the parent node of this node. This is 0 only for a root * node; otherwise this will be an actual Node */ KisNodeSP parent() const; /** * Returns the first child node of this node, or 0 if there are no * child nodes. */ KisNodeSP firstChild() const; /** * Returns the last child node of this node, or 0 if there are no * child nodes. */ KisNodeSP lastChild() const; /** * Returns the previous sibling of this node in the parent's list. * This is the node *above* this node in the composition stack. 0 * is returned if this child has no more previous siblings (== * firstChild()) */ KisNodeSP prevSibling() const; /** * Returns the next sibling of this node in the parent's list. * This is the node *below* this node in the composition stack. 0 * is returned if this child has no more next siblings (== * lastChild()) */ KisNodeSP nextSibling() const; /** * Returns how many direct child nodes this node has (not * recursive). */ quint32 childCount() const; /** * Retrieve the child node at the specified index. * * @return 0 if there is no node at this index. */ KisNodeSP at(quint32 index) const; /** * Retrieve the index of the specified child node. * * @return -1 if the specified node is not a child node of this * node. */ int index(const KisNodeSP node) const; /** * Return a list of child nodes of the current node that conform * to the specified constraints. There are no guarantees about the * order of the nodes in the list. The function is not recursive. * * @param nodeTypes. if not empty, only nodes that inherit the * classnames in this stringlist will be returned. * @param properties. if not empty, only nodes for which * KisNodeBase::check(properties) returns true will be returned. */ QList childNodes(const QStringList & nodeTypes, const KoProperties & properties) const; /** * @brief findChildByName finds the first child that has the given name * @param name the name to look for * @return the first child with the given name */ KisNodeSP findChildByName(const QString &name); Q_SIGNALS: /** * Don't use this signal anywhere other than KisNodeShape. It's a hack. */ void sigNodeChangedInternal(); public: /** * @return the node progress proxy used by this node, if this node has no progress * proxy, it will return the proxy of its parent, if the parent has no progress proxy * it will return 0 */ KisNodeProgressProxy* nodeProgressProxy() const; KisBusyProgressIndicator* busyProgressIndicator() const; private: /** * Create a node progress proxy for this node. You need to create a progress proxy only * if the node is going to appear in the layerbox, and it needs to be created before * the layer box is made aware of the proxy. */ void createNodeProgressProxy(); protected: KisBaseNodeSP parentCallback() const override; void notifyParentVisibilityChanged(bool value) override; void baseNodeChangedCallback() override; void baseNodeInvalidateAllFramesCallback() override; void baseNodeCollapsedChangedCallback() override; protected: void addKeyframeChannel(KisKeyframeChannel* channel) override; private: friend class KisNodeFacade; friend class KisNodeTest; friend class KisLayer; // Note: only for setting the preview mask! /** * Set the parent of this node. */ void setParent(KisNodeWSP parent); /** * Add the specified node above the specified node. If aboveThis * is 0, the node is added at the bottom. */ bool add(KisNodeSP newNode, KisNodeSP aboveThis); /** * Removes the node at the specified index from the child nodes. * * @return false if there is no node at this index */ bool remove(quint32 index); /** * Removes the node from the child nodes. * * @return false if there's no such node in this node. */ bool remove(KisNodeSP node); KisNodeSP prevChildImpl(KisNodeSP child); KisNodeSP nextChildImpl(KisNodeSP child); private: struct Private; Private * const m_d; }; Q_DECLARE_METATYPE(KisNodeSP) Q_DECLARE_METATYPE(KisNodeWSP) #endif diff --git a/libs/image/tiles3/kis_hline_iterator.cpp b/libs/image/tiles3/kis_hline_iterator.cpp index fe5558845e..66b8487884 100644 --- a/libs/image/tiles3/kis_hline_iterator.cpp +++ b/libs/image/tiles3/kis_hline_iterator.cpp @@ -1,231 +1,231 @@ /* * Copyright (c) 2010 Lukáš Tvrdý * * 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_hline_iterator.h" KisHLineIterator2::KisHLineIterator2(KisDataManager *dataManager, qint32 x, qint32 y, qint32 w, qint32 offsetX, qint32 offsetY, bool writable, KisIteratorCompleteListener *competionListener) : KisBaseIterator(dataManager, writable, competionListener), m_offsetX(offsetX), m_offsetY(offsetY) { x -= m_offsetX; y -= m_offsetY; Q_ASSERT(dataManager); if (w < 1) w = 1; // To make sure there's always at least one pixel read. m_x = x; m_y = y; m_left = x; m_right = x + w - 1; m_top = y; m_havePixels = (w == 0) ? false : true; if (m_left > m_right) { m_havePixels = false; return; } m_leftCol = xToCol(m_left); m_rightCol = xToCol(m_right); m_row = yToRow(m_y); m_yInTile = calcYInTile(m_y, m_row); m_leftInLeftmostTile = m_left - m_leftCol * KisTileData::WIDTH; m_tilesCacheSize = m_rightCol - m_leftCol + 1; m_tilesCache.resize(m_tilesCacheSize); m_tileWidth = m_pixelSize * KisTileData::HEIGHT; - // let's prealocate first row + // let's preallocate first row for (quint32 i = 0; i < m_tilesCacheSize; i++){ fetchTileDataForCache(m_tilesCache[i], m_leftCol + i, m_row); } m_index = 0; switchToTile(m_leftInLeftmostTile); } void KisHLineIterator2::resetPixelPos() { m_x = m_left; m_index = 0; switchToTile(m_leftInLeftmostTile); m_havePixels = true; } void KisHLineIterator2::resetRowPos() { m_y = m_top; m_row = yToRow(m_y); m_yInTile = calcYInTile(m_y, m_row); preallocateTiles(); resetPixelPos(); } bool KisHLineIterator2::nextPixel() { // We won't increment m_x here as integer can overflow here if (m_x >= m_right) { //return !m_isDoneFlag; return m_havePixels = false; } else { ++m_x; m_data += m_pixelSize; if (m_x <= m_rightmostInTile) m_oldData += m_pixelSize; else { // Switching to the beginning of the next tile ++m_index; switchToTile(0); } } return m_havePixels; } void KisHLineIterator2::nextRow() { m_x = m_left; ++m_y; if (++m_yInTile < KisTileData::HEIGHT) { /* do nothing, usual case */ } else { ++m_row; m_yInTile = 0; preallocateTiles(); } m_index = 0; switchToTile(m_leftInLeftmostTile); m_havePixels = true; } qint32 KisHLineIterator2::nConseqPixels() const { return qMin(m_rightmostInTile, m_right) - m_x + 1; } bool KisHLineIterator2::nextPixels(qint32 n) { Q_ASSERT_X(!(m_x > 0 && (m_x + n) < 0), "hlineIt+=", "Integer overflow"); qint32 previousCol = xToCol(m_x); // We won't increment m_x here first as integer can overflow if (m_x >= m_right || (m_x += n) > m_right) { m_havePixels = false; } else { qint32 col = xToCol(m_x); // if we are in the same column in tiles if (col == previousCol) { m_data += n * m_pixelSize; } else { qint32 xInTile = calcXInTile(m_x, col); m_index += col - previousCol; switchToTile(xInTile); } } return m_havePixels; } KisHLineIterator2::~KisHLineIterator2() { for (uint i = 0; i < m_tilesCacheSize; i++) { unlockTile(m_tilesCache[i].tile); unlockOldTile(m_tilesCache[i].oldtile); } } quint8* KisHLineIterator2::rawData() { return m_data; } const quint8* KisHLineIterator2::oldRawData() const { return m_oldData; } const quint8* KisHLineIterator2::rawDataConst() const { return m_data; } void KisHLineIterator2::switchToTile(qint32 xInTile) { // The caller must ensure that we are not out of bounds Q_ASSERT(m_index < m_tilesCacheSize); m_data = m_tilesCache[m_index].data; m_oldData = m_tilesCache[m_index].oldData; int offset_row = m_pixelSize * (m_yInTile * KisTileData::WIDTH); m_data += offset_row; m_rightmostInTile = (m_leftCol + m_index + 1) * KisTileData::WIDTH - 1; int offset_col = m_pixelSize * xInTile; m_data += offset_col; m_oldData += offset_row + offset_col; } void KisHLineIterator2::fetchTileDataForCache(KisTileInfo& kti, qint32 col, qint32 row) { m_dataManager->getTilesPair(col, row, m_writable, &kti.tile, &kti.oldtile); lockTile(kti.tile); kti.data = kti.tile->data(); lockOldTile(kti.oldtile); kti.oldData = kti.oldtile->data(); } void KisHLineIterator2::preallocateTiles() { for (quint32 i = 0; i < m_tilesCacheSize; ++i){ unlockTile(m_tilesCache[i].tile); unlockOldTile(m_tilesCache[i].oldtile); fetchTileDataForCache(m_tilesCache[i], m_leftCol + i, m_row); } } qint32 KisHLineIterator2::x() const { return m_x + m_offsetX; } qint32 KisHLineIterator2::y() const { return m_y + m_offsetY; } diff --git a/libs/image/tiles3/kis_memento_manager.h b/libs/image/tiles3/kis_memento_manager.h index 0b20aaa852..cb9761acf1 100644 --- a/libs/image/tiles3/kis_memento_manager.h +++ b/libs/image/tiles3/kis_memento_manager.h @@ -1,174 +1,174 @@ /* * 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. */ #ifndef KIS_MEMENTO_MANAGER_ #define KIS_MEMENTO_MANAGER_ #include #include "kis_memento_item.h" #include "config-hash-table-implementaion.h" typedef QList KisMementoItemList; typedef QListIterator KisMementoItemListIterator; class KisMemento; struct KisHistoryItem { KisMemento* memento; KisMementoItemList itemList; }; typedef QList KisHistoryList; class KisMemento; typedef KisSharedPtr KisMementoSP; #ifdef USE_LOCK_FREE_HASH_TABLE #include "kis_tile_hash_table2.h" typedef KisTileHashTableTraits2 KisMementoItemHashTable; typedef KisTileHashTableIteratorTraits2 KisMementoItemHashTableIterator; typedef KisTileHashTableIteratorTraits2 KisMementoItemHashTableIteratorConst; #else #include "kis_tile_hash_table.h" typedef KisTileHashTableTraits KisMementoItemHashTable; typedef KisTileHashTableIteratorTraits KisMementoItemHashTableIterator; typedef KisTileHashTableIteratorTraits KisMementoItemHashTableIteratorConst; #endif // USE_LOCK_FREE_HASH_TABLE class KRITAIMAGE_EXPORT KisMementoManager { public: KisMementoManager(); KisMementoManager(const KisMementoManager& rhs); ~KisMementoManager(); /** * Most tricky part. This function is called by a tile, when it gets new * tile-data through COW. The Memento Manager wraps this tile-data into * KisMementoItem class and waits until commit() order given. By this * time KisMementoItem doesn't take part in COW mechanism. It only holds * tileData->m_refCount counter to ensure tile isn't deleted from memory. * When commit() comes, KisMementoItem grabs tileData->m_usersCount and * since that moment it is a rightful co-owner of the tileData and COW * participant. It means that tileData won't be ever changed since then. * Every write request to the original tile will lead to duplicating * tileData and registering it here again... */ void registerTileChange(KisTile *tile); /** * Called when a tile deleted. Creates empty KisMementoItem showing that * there was a tile one day */ void registerTileDeleted(KisTile *tile); /** * Commits changes, made in INDEX: appends m_index into m_revisions list * and owes all modified tileDatas. */ void commit(); /** * Undo and Redo stuff respectively. * * When calling them, INDEX list should be empty, so to say, "working * copy should be clean". */ void rollback(KisTileHashTable *ht); void rollforward(KisTileHashTable *ht); /** * Get old tile, whose memento is in the HEAD revision. * \p existingTile returns if the tile is actually an existing * non-default tile or it was created on the fly * from the default tile data */ KisTileSP getCommitedTile(qint32 col, qint32 row, bool &existingTile); KisMementoSP getMemento(); bool hasCurrentMemento() { return m_currentMemento; } KisMementoSP currentMemento(); void setDefaultTileData(KisTileData *defaultTileData); void debugPrintInfo(); /** - * Removes all the history that preceds the revision + * Removes all the history that precedes the revision * pointed by oldestMemento. That is after calling to * purgeHistory(someMemento) you won't be able to do * rollback(someMemento) anymore. */ void purgeHistory(KisMementoSP oldestMemento); protected: qint32 findRevisionByMemento(KisMementoSP memento) const; void resetRevisionHistory(KisMementoItemList list); protected: /** * INDEX of tiles to be committed with next commit() * We use a hash table to be able to check that * we have the only memento item for a tile * per commit efficiently */ KisMementoItemHashTable m_index; /** * Main list that stores every commit ever done */ KisHistoryList m_revisions; /** * List of revisions temporarily undone while rollback() */ KisHistoryList m_cancelledRevisions; /** * A hash table, that stores the most recently updated * versions of tiles. Say, HEAD revision :) */ KisMementoItemHashTable m_headsHashTable; /** * Stores extent of current INDEX. * It is the "name" of current named transaction */ KisMementoSP m_currentMemento; /** * The flag that blocks registration of changes on tiles. * This is a temporary state of the memento manager, that * is used for traveling in history * * \see rollback() * \see rollforward() */ bool m_registrationBlocked; }; #endif /* KIS_MEMENTO_MANAGER_ */ diff --git a/libs/image/tiles3/kis_tile.h b/libs/image/tiles3/kis_tile.h index 7b40b4e6d5..73d197bdc0 100644 --- a/libs/image/tiles3/kis_tile.h +++ b/libs/image/tiles3/kis_tile.h @@ -1,193 +1,193 @@ /* * Copyright (c) 2004 C. Boemann * (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. */ #ifndef KIS_TILE_H_ #define KIS_TILE_H_ #include #include #include #include #include #include #include #include "kis_tile_data.h" #include "kis_tile_data_store.h" //#define DEAD_TILES_SANITY_CHECK class KisTile; typedef KisSharedPtr KisTileSP; class KisMementoManager; /** * Provides abstraction to a tile. * + A tile contains a part of a PaintDevice, - * but only the individual pixels are accesable + * but only the individual pixels are accessible * and that only via iterators. * + Actual tile data is stored in KisTileData that can be * shared between many tiles */ class KRITAIMAGE_EXPORT KisTile : public KisShared { public: KisTile(qint32 col, qint32 row, KisTileData *defaultTileData, KisMementoManager* mm); KisTile(const KisTile& rhs, qint32 col, qint32 row, KisMementoManager* mm); KisTile(const KisTile& rhs, KisMementoManager* mm); KisTile(const KisTile& rhs); ~KisTile(); /** * This method is called by the hash table when the tile is * disconnected from it. It means that from now on the tile is not * associated with any particular datamanager. All the users of * the tile (via shared pointers) may silently finish they work on * this tile and leave it. No result will be saved. Used for * threading purposes */ void notifyDetachedFromDataManager(); /** * Sometimes the tile gets replaced with another tile. In this case * we shouldn't notify memento manager that the tile has died. Just * forget the link to the manager and bury it in peace. */ void notifyDeadWithoutDetaching(); /** * Called by the hash table to notify that the tile has been attached * to the data manager. */ void notifyAttachedToDataManager(KisMementoManager *mm); public: void debugPrintInfo(); void debugDumpTile(); void lockForRead() const; void lockForWrite(); void unlockForWrite(); void unlockForRead() const; /* this allows us work directly on tile's data */ inline quint8 *data() const { return m_tileData->data(); } inline void setData(const quint8 *data) { m_tileData->setData(data); } inline qint32 row() const { return m_row; } inline qint32 col() const { return m_col; } inline QRect extent() const { return m_extent; //QRect(m_col * KisTileData::WIDTH, m_row * KisTileData::HEIGHT, // KisTileData::WIDTH, KisTileData::HEIGHT); } inline KisTileSP next() const { return m_nextTile; } void setNext(KisTileSP next) { m_nextTile = next; } inline qint32 pixelSize() const { /* don't lock here as pixelSize is constant */ return m_tileData->pixelSize(); } inline KisTileData* tileData() const { return m_tileData; } private: void init(qint32 col, qint32 row, KisTileData *defaultTileData, KisMementoManager* mm); inline void blockSwapping() const; inline void unblockSwapping() const; inline void safeReleaseOldTileData(KisTileData *td); private: KisTileData *m_tileData; mutable QStack m_oldTileData; mutable volatile int m_lockCounter; qint32 m_col; qint32 m_row; /** * Added for faster retrieving by processors */ QRect m_extent; /** * For KisTiledDataManager's hash table */ KisTileSP m_nextTile; QAtomicPointer m_mementoManager; /** * This is a special mutex for guarding copy-on-write * operations. We do not use lockless way here as it'll * create too much overhead for the most common operations * like "read the pointer of m_tileData". */ QMutex m_COWMutex; /** * This lock is used to ensure no one will read the tile data * before it has been loaded from to the memory. */ mutable QMutex m_swapBarrierLock; #ifdef DEAD_TILES_SANITY_CHECK QAtomicInt m_sanityHasBeenDetached; QAtomicInt m_sanityIsDead; QAtomicInt m_sanityMMHasBeenInitializedManually; QAtomicInt m_sanityNumCOWHappened; QAtomicInt m_sanityLockedForWrite; mutable QAtomicInt m_sanityLockedForRead; void sanityCheckIsNotDestroyedYet(); void sanityCheckIsNotLockedForWrite(); #endif }; #endif // KIS_TILE_H_ diff --git a/libs/image/tiles3/kis_tile_data_interface.h b/libs/image/tiles3/kis_tile_data_interface.h index 2874a59b56..eb9cf13719 100644 --- a/libs/image/tiles3/kis_tile_data_interface.h +++ b/libs/image/tiles3/kis_tile_data_interface.h @@ -1,327 +1,327 @@ /* * Copyright (c) 2009 Dmitry Kazakov * Copyright (c) 2018 Andrey Kamakin * * 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_TILE_DATA_INTERFACE_H_ #define KIS_TILE_DATA_INTERFACE_H_ #include #include #include "kis_lockless_stack.h" #include "swap/kis_chunk_allocator.h" class KisTileData; class KisTileDataStore; /** * WARNING: Those definitions for internal use only! * Please use KisTileData::WIDTH/HEIGHT instead */ #define __TILE_DATA_WIDTH 64 #define __TILE_DATA_HEIGHT 64 typedef KisLocklessStack KisTileDataCache; typedef QLinkedList KisTileDataList; typedef KisTileDataList::iterator KisTileDataListIterator; typedef KisTileDataList::const_iterator KisTileDataListConstIterator; class SimpleCache { public: SimpleCache() = default; ~SimpleCache(); bool push(int pixelSize, quint8 *&ptr) { QReadLocker l(&m_cacheLock); switch (pixelSize) { case 4: m_4Pool.push(ptr); break; case 8: m_8Pool.push(ptr); break; case 16: m_16Pool.push(ptr); break; default: return false; } return true; } bool pop(int pixelSize, quint8 *&ptr) { QReadLocker l(&m_cacheLock); switch (pixelSize) { case 4: return m_4Pool.pop(ptr); case 8: return m_8Pool.pop(ptr); case 16: return m_16Pool.pop(ptr); default: return false; } } void clear(); private: QReadWriteLock m_cacheLock; KisLocklessStack m_4Pool; KisLocklessStack m_8Pool; KisLocklessStack m_16Pool; }; /** * Stores actual tile's data */ class KRITAIMAGE_EXPORT KisTileData { public: KisTileData(qint32 pixelSize, const quint8 *defPixel, KisTileDataStore *store, bool checkFreeMemory = true); private: KisTileData(const KisTileData& rhs, bool checkFreeMemory = true); public: ~KisTileData(); enum EnumTileDataState { NORMAL = 0, COMPRESSED, SWAPPED }; /** * Information about data stored */ inline quint8* data() const; inline void setData(const quint8 *data); inline quint32 pixelSize() const; /** * Increments usersCount of a TD and refs shared pointer counter * Used by KisTile for COW */ inline bool acquire(); /** * Decrements usersCount of a TD and derefs shared pointer counter * Used by KisTile for COW */ inline bool release(); /** * Only refs shared pointer counter. * Used only by KisMementoManager without * consideration of COW. */ inline bool ref() const; /** * Only refs shared pointer counter. * Used only by KisMementoManager without * consideration of COW. */ inline bool deref(); /** * Creates a clone of the tile data safely. * It will try to use the cached clones. */ inline KisTileData* clone(); /** * Control the access of swapper to the tile data */ inline void blockSwapping(); inline void unblockSwapping(); /** * The position of the tile data in a swap file */ inline KisChunk swapChunk() const; inline void setSwapChunk(KisChunk chunk); /** * Show whether a tile data is a part of history */ inline bool mementoed() const; inline void setMementoed(bool value); /** * Controlling methods for setting 'age' marks */ inline int age() const; inline void resetAge(); inline void markOld(); /** * Returns number of tiles (or memento items), * referencing the tile data. */ inline qint32 numUsers() const; /** - * Conveniece method. Returns true iff the tile data is linked to + * Convenience method. Returns true iff the tile data is linked to * information only and therefore can be swapped out easily. * * Effectively equivalent to: (mementoed() && numUsers() <= 1) */ inline bool historical() const; /** * Used for swapping purposes only. * Frees the memory occupied by the tile data. * (the caller must save the data beforehand) */ void releaseMemory(); /** * Used for swapping purposes only. * Allocates memory for the tile data after * it has been freed in releaseMemory(). * NOTE: the new data can be not-initialized * and you must fill it yourself! * * \see releaseMemory() */ void allocateMemory(); /** * Releases internal pools, which keep blobs where the tiles are * stored. The point is that we don't allocate the tiles from * glibc directly, but use pools (implemented via boost) to * allocate bigger chunks. This method should be called when one * knows that we have just free'd quite a lot of memory and we * won't need it anymore. E.g. when a document has been closed. */ static void releaseInternalPools(); private: void fillWithPixel(const quint8 *defPixel); static quint8* allocateData(const qint32 pixelSize); static void freeData(quint8 *ptr, const qint32 pixelSize); private: friend class KisTileDataPooler; friend class KisTileDataPoolerTest; /** * A list of pre-duplicated tiledatas. * To make a COW faster, KisTileDataPooler thread duplicates * a tile beforehand and stores clones here, in this stack */ KisTileDataCache m_clonesStack; private: friend class KisTile; friend class KisTileDataStore; friend class KisTileDataStoreIterator; friend class KisTileDataStoreReverseIterator; friend class KisTileDataStoreClockIterator; /** * The state of the tile. * Filled in by tileDataStore and * checked in KisTile::acquireFor* * see also: comment for @m_data */ mutable EnumTileDataState m_state; /** * Iterator that points to a position in the list * where the tile data is stored */ int m_tileNumber = -1; private: /** * The chunk of the swap file, that corresponds * to this tile data. Used by KisSwappedDataStore. */ KisChunk m_swapChunk; /** * The flag is set by KisMementoItem to show this * tile data is going down in history. * * (m_mementoFlag && m_usersCount == 1) means that * the only user of tile data is a memento manager. */ qint32 m_mementoFlag; /** * Counts up time after last access to the tile data. * 0 - recently accessed * 1+ - not recently accessed */ //FIXME: make memory aligned int m_age; /** * The primitive for controlling swapping of the tile. * lockForRead() - used by regular threads to ensure swapper * won't touch this tile data. * tryLockForWrite() - used by swapper to check no-one reads * this tile data */ QReadWriteLock m_swapLock; private: friend class KisLowMemoryTests; /** * FIXME: We should be able to work in const environment * even when actual data is swapped out to disk */ mutable quint8* m_data; /** * How many tiles/mementoes use * this tiledata through COW? */ mutable QAtomicInt m_usersCount; /** * Shared pointer counter */ mutable QAtomicInt m_refCount; qint32 m_pixelSize; //qint32 m_timeStamp; KisTileDataStore *m_store; static SimpleCache m_cache; public: static const qint32 WIDTH; static const qint32 HEIGHT; }; #endif /* KIS_TILE_DATA_INTERFACE_H_ */ diff --git a/libs/image/tiles3/kis_vline_iterator.cpp b/libs/image/tiles3/kis_vline_iterator.cpp index 732d12f194..8a1c7086fd 100644 --- a/libs/image/tiles3/kis_vline_iterator.cpp +++ b/libs/image/tiles3/kis_vline_iterator.cpp @@ -1,234 +1,234 @@ /* * Copyright (c) 2010 Lukáš Tvrdý * Copyright (c) 2010 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_vline_iterator.h" #include KisVLineIterator2::KisVLineIterator2(KisDataManager *dataManager, qint32 x, qint32 y, qint32 h, qint32 offsetX, qint32 offsetY, bool writable, KisIteratorCompleteListener *completeListener) : KisBaseIterator(dataManager, writable, completeListener), m_offsetX(offsetX), m_offsetY(offsetY) { x -= m_offsetX; y -= m_offsetY; Q_ASSERT(dataManager != 0); Q_ASSERT(h > 0); // for us, to warn us when abusing the iterators if (h < 1) h = 1; // for release mode, to make sure there's always at least one pixel read. m_lineStride = m_pixelSize * KisTileData::WIDTH; m_x = x; m_y = y; m_top = y; m_bottom = y + h - 1; m_left = m_x; m_havePixels = (h == 0) ? false : true; if (m_top > m_bottom) { m_havePixels = false; return; } m_topRow = yToRow(m_top); m_bottomRow = yToRow(m_bottom); m_column = xToCol(m_x); m_xInTile = calcXInTile(m_x, m_column); m_topInTopmostTile = m_top - m_topRow * KisTileData::WIDTH; m_tilesCacheSize = m_bottomRow - m_topRow + 1; m_tilesCache.resize(m_tilesCacheSize); m_tileSize = m_lineStride * KisTileData::HEIGHT; - // let's prealocate first row + // let's preallocate first row for (int i = 0; i < m_tilesCacheSize; i++){ fetchTileDataForCache(m_tilesCache[i], m_column, m_topRow + i); } m_index = 0; switchToTile(m_topInTopmostTile); } void KisVLineIterator2::resetPixelPos() { m_y = m_top; m_index = 0; switchToTile(m_topInTopmostTile); m_havePixels = true; } void KisVLineIterator2::resetColumnPos() { m_x = m_left; m_column = xToCol(m_x); m_xInTile = calcXInTile(m_x, m_column); preallocateTiles(); resetPixelPos(); } bool KisVLineIterator2::nextPixel() { // We won't increment m_x here as integer can overflow here if (m_y >= m_bottom) { //return !m_isDoneFlag; return m_havePixels = false; } else { ++m_y; m_data += m_lineStride; if (m_data < m_dataBottom) m_oldData += m_lineStride; else { // Switching to the beginning of the next tile ++m_index; switchToTile(0); } } return m_havePixels; } void KisVLineIterator2::nextColumn() { m_y = m_top; ++m_x; if (++m_xInTile < KisTileData::HEIGHT) { /* do nothing, usual case */ } else { ++m_column; m_xInTile = 0; preallocateTiles(); } m_index = 0; switchToTile(m_topInTopmostTile); m_havePixels = true; } qint32 KisVLineIterator2::nConseqPixels() const { return 1; } bool KisVLineIterator2::nextPixels(qint32 n) { Q_ASSERT_X(!(m_y > 0 && (m_y + n) < 0), "vlineIt+=", "Integer overflow"); qint32 previousRow = yToRow(m_y); // We won't increment m_x here first as integer can overflow if (m_y >= m_bottom || (m_y += n) > m_bottom) { m_havePixels = false; } else { qint32 row = yToRow(m_y); // if we are in the same column in tiles if (row == previousRow) { m_data += n * m_pixelSize; } else { qint32 yInTile = calcYInTile(m_y, row); m_index += row - previousRow; switchToTile(yInTile); } } return m_havePixels; } KisVLineIterator2::~KisVLineIterator2() { for (int i = 0; i < m_tilesCacheSize; i++) { unlockTile(m_tilesCache[i].tile); unlockOldTile(m_tilesCache[i].oldtile); } } quint8* KisVLineIterator2::rawData() { return m_data; } const quint8* KisVLineIterator2::oldRawData() const { return m_oldData; } const quint8* KisVLineIterator2::rawDataConst() const { return m_data; } void KisVLineIterator2::switchToTile(qint32 yInTile) { // The caller must ensure that we are not out of bounds Q_ASSERT(m_index < m_tilesCacheSize); Q_ASSERT(m_index >= 0); int offset_row = m_pixelSize * m_xInTile; m_data = m_tilesCache[m_index].data; m_oldData = m_tilesCache[m_index].oldData; m_data += offset_row; m_dataBottom = m_data + m_tileSize; int offset_col = m_pixelSize * yInTile * KisTileData::WIDTH; m_data += offset_col; m_oldData += offset_row + offset_col; } void KisVLineIterator2::fetchTileDataForCache(KisTileInfo& kti, qint32 col, qint32 row) { m_dataManager->getTilesPair(col, row, m_writable, &kti.tile, &kti.oldtile); lockTile(kti.tile); kti.data = kti.tile->data(); lockOldTile(kti.oldtile); kti.oldData = kti.oldtile->data(); } void KisVLineIterator2::preallocateTiles() { for (int i = 0; i < m_tilesCacheSize; ++i){ unlockTile(m_tilesCache[i].tile); unlockOldTile(m_tilesCache[i].oldtile); fetchTileDataForCache(m_tilesCache[i], m_column, m_topRow + i ); } } qint32 KisVLineIterator2::x() const { return m_x + m_offsetX; } qint32 KisVLineIterator2::y() const { return m_y + m_offsetY; } diff --git a/libs/image/tiles3/tests/kis_lockless_stack_test.cpp b/libs/image/tiles3/tests/kis_lockless_stack_test.cpp index 5fe713854a..91f35828fc 100644 --- a/libs/image/tiles3/tests/kis_lockless_stack_test.cpp +++ b/libs/image/tiles3/tests/kis_lockless_stack_test.cpp @@ -1,352 +1,352 @@ /* * Copyright (c) 2010 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_lockless_stack_test.h" #include #include "kis_debug.h" #include "tiles3/kis_lockless_stack.h" #include "config-limit-long-tests.h" void KisLocklessStackTest::testOperations() { KisLocklessStack stack; for(qint32 i = 0; i < 1024; i++) { stack.push(i); } QCOMPARE(stack.size(), 1024); for(qint32 i = 1023; i >= 0; i--) { int value; bool result = stack.pop(value); QVERIFY(result); QCOMPARE(value, i); } QVERIFY(stack.isEmpty()); } -/************ BENCHMARKING INFRASTRACTURE ************************/ +/************ BENCHMARKING INFRASTRUCTURE ************************/ #define NUM_TYPES 2 // high-concurrency #define NUM_CYCLES 500000 #define NUM_THREADS 10 // relaxed //#define NUM_CYCLES 100 //#define NUM_THREADS 2 // single-threaded //#define NUM_CYCLES 10000000 //#define NUM_THREADS 1 class KisAbstractIntStack { public: virtual ~KisAbstractIntStack() {} virtual void push(int value) = 0; virtual int pop() = 0; virtual bool isEmpty() = 0; virtual void clear() = 0; }; class KisTestingLocklessStack : public KisAbstractIntStack { public: void push(int value) override { m_stack.push(value); } int pop() override { int value = 0; bool result = m_stack.pop(value); Q_ASSERT(result); Q_UNUSED(result); // for release build return value; } bool isEmpty() override { return m_stack.isEmpty(); } void clear() override { m_stack.clear(); } private: KisLocklessStack m_stack; }; class KisTestingLegacyStack : public KisAbstractIntStack { public: void push(int value) override { m_mutex.lock(); m_stack.push(value); m_mutex.unlock(); } int pop() override { m_mutex.lock(); int result = m_stack.pop(); m_mutex.unlock(); return result; } bool isEmpty() override { m_mutex.lock(); bool result = m_stack.isEmpty(); m_mutex.unlock(); return result; } void clear() override { m_mutex.lock(); m_stack.clear(); m_mutex.unlock(); } private: QStack m_stack; QMutex m_mutex; }; class KisStressJob : public QRunnable { public: KisStressJob(KisAbstractIntStack &stack, qint32 startValue) : m_stack(stack), m_startValue(startValue) { m_pushSum = 0; m_popSum = 0; } void run() override { for(qint32 i = 0; i < NUM_CYCLES; i++) { qint32 type = i % NUM_TYPES; int newValue; switch(type) { case 0: newValue = m_startValue + i; m_pushSum += newValue; m_stack.push(newValue); break; case 1: m_popSum += m_stack.pop(); break; } } } qint64 pushSum() { return m_pushSum; } qint64 popSum() { return m_popSum; } private: KisAbstractIntStack &m_stack; qint32 m_startValue; qint64 m_pushSum; qint64 m_popSum; }; void KisLocklessStackTest::runStressTest(KisAbstractIntStack &stack) { QList jobsList; KisStressJob *job; for(qint32 i = 0; i < NUM_THREADS; i++) { job = new KisStressJob(stack, 1); job->setAutoDelete(false); jobsList.append(job); } QThreadPool pool; pool.setMaxThreadCount(NUM_THREADS); QBENCHMARK { Q_FOREACH (job, jobsList) { pool.start(job); } pool.waitForDone(); } QVERIFY(stack.isEmpty()); qint64 totalSum = 0; for(qint32 i = 0; i < NUM_THREADS; i++) { KisStressJob *job = jobsList.takeLast(); totalSum += job->pushSum(); totalSum -= job->popSum(); dbgKrita << ppVar(totalSum); delete job; } QCOMPARE(totalSum, (long long) 0); } void KisLocklessStackTest::stressTestLockless() { KisTestingLocklessStack stack; runStressTest(stack); } void KisLocklessStackTest::stressTestQStack() { KisTestingLegacyStack stack; runStressTest(stack); } class KisStressClearJob : public QRunnable { public: KisStressClearJob(KisLocklessStack &stack, qint32 startValue) : m_stack(stack), m_startValue(startValue) { } void run() override { for(qint32 i = 0; i < NUM_CYCLES; i++) { qint32 type = i % 4; int newValue; switch(type) { case 0: case 1: newValue = m_startValue + i; m_stack.push(newValue); break; case 2: int tmp; m_stack.pop(tmp); break; case 3: m_stack.clear(); break; } } } private: KisLocklessStack &m_stack; qint32 m_startValue; }; void KisLocklessStackTest::stressTestClear() { KisLocklessStack stack; KisStressClearJob *job; QThreadPool pool; pool.setMaxThreadCount(NUM_THREADS); for(qint32 i = 0; i < NUM_THREADS; i++) { job = new KisStressClearJob(stack, 1); pool.start(job); } pool.waitForDone(); stack.clear(); QVERIFY(stack.isEmpty()); } class KisStressBulkPopJob : public QRunnable { public: KisStressBulkPopJob(KisLocklessStack &stack, qint64 &removedCheckSum) : m_stack(stack), m_removedCheckSum(removedCheckSum) { } void run() override { int value = 0; while (m_stack.pop(value)) { m_removedCheckSum += value; } } private: KisLocklessStack &m_stack; qint64 &m_removedCheckSum; }; void KisLocklessStackTest::stressTestBulkPop() { qsrand(10); KisLocklessStack stack; #ifdef LIMIT_LONG_TESTS const int numThreads = 3; const int numObjects = 10000000; #else const int numThreads = 3; const int numObjects = 10000000; #endif QThreadPool pool; pool.setMaxThreadCount(numThreads); qint64 expectedSum = 0; for (int i = 0; i < numObjects; i++) { const int value = i % 3 + 1; expectedSum += value; stack.push(value); } QVector partialSums(numThreads); for(qint32 i = 0; i < numThreads; i++) { KisStressBulkPopJob *job = new KisStressBulkPopJob(stack, partialSums[i]); pool.start(job); } pool.waitForDone(); QVERIFY(stack.isEmpty()); const qint64 realSum = std::accumulate(partialSums.begin(), partialSums.end(), 0); QCOMPARE(realSum, expectedSum); } QTEST_MAIN(KisLocklessStackTest) diff --git a/libs/libkis/Resource.h b/libs/libkis/Resource.h index 8ed2052c11..87a6c0f05b 100644 --- a/libs/libkis/Resource.h +++ b/libs/libkis/Resource.h @@ -1,121 +1,121 @@ /* * Copyright (c) 2016 Boudewijn Rempt * * This program 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 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 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. */ #ifndef LIBKIS_RESOURCE_H #define LIBKIS_RESOURCE_H #include #include #include "kritalibkis_export.h" #include "libkis.h" class KoResource; /** * A Resource represents a gradient, pattern, brush tip, brush preset, palette or * workspace definition. * * @code * allPresets = Application.resources("preset") * for preset in allPresets: * print(preset.name()) * @endcode * * Resources are identified by their type, name and filename. If you want to change * the contents of a resource, you should read its data using data(), parse it and * write the changed contents back. */ class KRITALIBKIS_EXPORT Resource : public QObject { Q_OBJECT public: explicit Resource(KoResource *resource, QObject *parent = 0); ~Resource() override; bool operator==(const Resource &other) const; bool operator!=(const Resource &other) const; public Q_SLOTS: /** * Return the type of this resource. Valid types are: *
    *
  • pattern: a raster image representing a pattern *
  • gradient: a gradient *
  • brush: a brush tip *
  • preset: a brush preset *
  • palette: a color set *
  • workspace: a workspace definition. *
*/ QString type() const; /** * The user-visible name of the resource. */ QString name() const; /** * setName changes the user-visible name of the current resource. */ void setName(QString value); /** * The filename of the resource, if present. Not all resources * are loaded from files. */ QString filename() const; /** * An image that can be used to represent the resource in the * user interface. For some resources, like patterns, the * image is identical to the resource, for others it's a mere * icon. */ QImage image() const; /** * Change the image for this resource. */ void setImage(QImage image); /** * Return the resource as a byte array. */ QByteArray data() const; /** * Change the internal data of the resource to the given byte * array. If the byte array is not valid, setData returns - * false, otherwwise true. + * false, otherwise true. */ bool setData(QByteArray data); private: friend class PresetChooser; friend class View; friend class Palette; KoResource *resource() const; struct Private; const Private *const d; }; #endif // LIBKIS_RESOURCE_H diff --git a/libs/odf/KoBorder.cpp b/libs/odf/KoBorder.cpp index c357ccee44..c546abe8ff 100644 --- a/libs/odf/KoBorder.cpp +++ b/libs/odf/KoBorder.cpp @@ -1,1164 +1,1164 @@ /* This file is part of the KDE project * * Copyright (C) 2009 Inge Wallin * Copyright (C) 2009 Thomas Zander * Copyright (C) 2011 Pierre Ducroquet * * 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 "KoBorder.h" #include #include #include #include #include #include class KoBorderPrivate : public QSharedData { public: KoBorderPrivate(); ~KoBorderPrivate(); QMap data; }; KoBorderPrivate::KoBorderPrivate() { } KoBorderPrivate::~KoBorderPrivate() { } KoBorder::BorderData::BorderData() : style(KoBorder::BorderNone) , outerPen(QPen()) , innerPen(QPen()) , spacing(0) { outerPen.setWidthF(0.0f); innerPen.setWidthF(0.0f); } bool KoBorder::BorderData::operator==(const KoBorder::BorderData& other) const { // Left Borders if (style == BorderNone && other.style == BorderNone) { // If both styles are None, then the rest of the values don't // need to be compared. ; } else if (style != other.style) { // If any of them are non-None, and they are different, the // borders are also different. return false; } else { // Here we know that the border styles are the same, now // compare the rest of the values. if (outerPen != other.outerPen) return false; // If the border style == BorderDouble, then compare a couple // of other values too. if (style == BorderDouble) { if (innerPen != other.innerPen) return false; if (spacing != other.spacing) return false; } } return true; } // ---------------------------------------------------------------- KoBorder::KoBorder() : d(new KoBorderPrivate) { } KoBorder::KoBorder(const KoBorder &kb) : d(kb.d) { } KoBorder::~KoBorder() { // No delete because d is a QSharedDataPointer. } // ---------------------------------------------------------------- // operators KoBorder &KoBorder::operator=(const KoBorder &other) { d = other.d; return *this; } bool KoBorder::operator==(const KoBorder &other) const { if (d.data() == other.d.data()) return true; if (d->data.size() != other.d->data.size()) return false; KoBorder::BorderSide key; foreach (key, d->data.keys()) { if (!other.d->data.contains(key)) return false; if (!(other.d->data[key] == d->data[key])) return false; } return true; } // ---------------------------------------------------------------- // public, non-class functions KoBorder::BorderStyle KoBorder::odfBorderStyle(const QString &borderstyle, bool *converted) { // Note: the styles marked "Not odf compatible" below are legacies // from the old words format. There are also lots of border // styles in the MS DOC that we may have to handle at some point. if (converted) *converted = true; if (borderstyle == "none") return BorderNone; if (borderstyle == "solid") return BorderSolid; if (borderstyle == "dashed") return BorderDashed; if (borderstyle == "dotted") return BorderDotted; if (borderstyle == "dot-dash") return BorderDashDot; if (borderstyle == "dot-dot-dash") return BorderDashDotDot; if (borderstyle == "double") return BorderDouble; if (borderstyle == "groove") // Not odf compatible -- see above return BorderGroove; if (borderstyle == "ridge") // Not odf compatible -- see above return BorderRidge; if (borderstyle == "inset") // Not odf compatible -- see above return BorderInset; if (borderstyle == "outset") // Not odf compatible -- see above return BorderOutset; if (borderstyle == "dash-largegap") return KoBorder::BorderDashedLong; if (borderstyle == "slash") // not officially odf, but we support it anyway return KoBorder::BorderSlash; if (borderstyle == "wave") // not officially odf, but we support it anyway return KoBorder::BorderWave; if (borderstyle == "double-wave") // not officially odf, but we support it anyway return KoBorder::BorderDoubleWave; if (converted) *converted = false; return BorderSolid; } QString KoBorder::odfBorderStyleString(BorderStyle borderstyle) { switch (borderstyle) { case BorderDashed: return QString("dashed"); case BorderDotted: return QString("dotted"); case BorderDashDot: return QString("dot-dash"); case BorderDashDotDot: return QString("dot-dot-dash"); case BorderDouble: return QString("double"); case BorderGroove: return QString("groove"); // not odf -- see above case BorderRidge: return QString("ridge"); // not odf -- see above case BorderInset: return QString("inset"); // not odf -- see above case BorderOutset: return QString("outset"); // not odf -- see above case BorderSolid: return QString("solid"); case BorderNone: return QString("none"); default: // Handle unknown types as solid. return QString("solid"); } } QString KoBorder::msoBorderStyleString(BorderStyle borderstyle) { switch (borderstyle) { case KoBorder::BorderDashedLong: return QString("dash-largegap"); case KoBorder::BorderSlash: return QString("slash"); // not officially odf, but we support it anyway case KoBorder::BorderWave: return QString("wave"); // not officially odf, but we support it anyway case KoBorder::BorderDoubleWave: return QString("double-wave"); // not officially odf, but we support it anyway default: // Handle remaining styles as odf type style. return odfBorderStyleString(borderstyle); } } // ---------------------------------------------------------------- // Getters and Setters void KoBorder::setBorderStyle(BorderSide side, BorderStyle style) { if (d->data[side].style == style) { return; } if (!d->data.contains(side)) { BorderData data; data.style = style; d->data[side] = data; } else { d->data[side].style = style; } // Make a best effort to create the best possible dash pattern for the chosen style. // FIXME: KoTableCellStyle::setEdge() should call this function. BorderData &edge = d->data[side]; qreal width = edge.outerPen.widthF(); qreal innerWidth = 0; qreal middleWidth = 0; qreal space = 0; QVector dashes; switch (style) { case KoBorder::BorderNone: width = 0.0; break; case KoBorder::BorderDouble: innerWidth = space = edge.outerPen.width() / 3; //some nice default look width -= (space + innerWidth); edge.outerPen.setStyle(Qt::SolidLine); break; case KoBorder::BorderDotted: dashes << 1 << 1; edge.outerPen.setDashPattern(dashes); break; case KoBorder::BorderDashed: dashes << 4 << 1; edge.outerPen.setDashPattern(dashes); break; case KoBorder::BorderDashedLong: { dashes << 4 << 4; edge.outerPen.setDashPattern(dashes); break; } case KoBorder::BorderTriple: innerWidth = middleWidth = space = width/6; width -= (space + innerWidth); edge.outerPen.setStyle(Qt::SolidLine); break; case KoBorder::BorderDashDot: dashes << 3 << 3<< 7 << 3; edge.outerPen.setDashPattern(dashes); break; case KoBorder::BorderDashDotDot: dashes << 2 << 2<< 6 << 2 << 2 << 2; edge.outerPen.setDashPattern(dashes); break; case KoBorder::BorderWave: edge.outerPen.setStyle(Qt::SolidLine); break; case KoBorder::BorderSlash: edge.outerPen.setStyle(Qt::SolidLine); break; case KoBorder::BorderDoubleWave: innerWidth = space = width/3; //some nice default look width -= (space + innerWidth); edge.outerPen.setStyle(Qt::SolidLine); break; default: edge.outerPen.setStyle(Qt::SolidLine); break; } edge.outerPen.setJoinStyle(Qt::MiterJoin); edge.outerPen.setCapStyle(Qt::FlatCap); edge.outerPen.setWidthF(width); edge.spacing = space; edge.innerPen = edge.outerPen; edge.innerPen.setWidthF(innerWidth); } KoBorder::BorderStyle KoBorder::borderStyle(BorderSide side) const { if (!d->data.contains(side)) { return BorderNone; } else { return d->data[side].style; } } void KoBorder::setBorderColor(BorderSide side, const QColor &color) { if (!d->data.contains(side)) { BorderData data; data.outerPen.setColor(color); d->data[side] = data; } else { d->data[side].outerPen.setColor(color); } } QColor KoBorder::borderColor(BorderSide side) const { if (!d->data.contains(side)) { return QColor(); } else { return d->data[side].outerPen.color(); } } void KoBorder::setBorderWidth(BorderSide side, qreal width) { if (!d->data.contains(side)) { BorderData data; data.outerPen.setWidthF(width); d->data[side] = data; } else { d->data[side].outerPen.setWidthF(width); } } qreal KoBorder::borderWidth(BorderSide side) const { if (!d->data.contains(side)) { return 0; } else { if (d->data[side].style == BorderDouble) return (d->data[side].outerPen.widthF() + d->data[side].innerPen.widthF() + d->data[side].spacing); else return d->data[side].outerPen.widthF(); } } void KoBorder::setOuterBorderWidth(BorderSide side, qreal width) { if (!d->data.contains(side)) { BorderData data; data.outerPen.setWidthF(width); d->data[side] = data; } else { d->data[side].outerPen.setWidthF(width); } } qreal KoBorder::outerBorderWidth(BorderSide side) const { if (!d->data.contains(side)) { return 0; } else { return d->data[side].outerPen.widthF(); } } void KoBorder::setInnerBorderWidth(BorderSide side, qreal width) { if (!d->data.contains(side)) { BorderData data; data.innerPen.setWidthF(width); d->data[side] = data; } else { d->data[side].innerPen.setWidthF(width); } } qreal KoBorder::innerBorderWidth(BorderSide side) const { if (!d->data.contains(side)) { return 0; } else { return d->data[side].innerPen.widthF(); } } void KoBorder::setBorderSpacing(BorderSide side, qreal width) { if (!d->data.contains(side)) { BorderData data; data.spacing = width; d->data[side] = data; } else { d->data[side].spacing = width; } } qreal KoBorder::borderSpacing(BorderSide side) const { if (!d->data.contains(side)) { return 0; } else { return d->data[side].spacing; } } KoBorder::BorderData KoBorder::borderData(BorderSide side) const { return d->data.value(side, BorderData()); } void KoBorder::setBorderData(BorderSide side, const BorderData &data) { d->data[side] = data; } // ------------------------------- bool KoBorder::hasBorder() const { if (borderStyle(LeftBorder) != BorderNone && borderWidth(LeftBorder) > 0.0) return true; if (borderStyle(RightBorder) != BorderNone && borderWidth(RightBorder) > 0.0) return true; if (borderStyle(TopBorder) != BorderNone && borderWidth(TopBorder) > 0.0) return true; if (borderStyle(BottomBorder) != BorderNone && borderWidth(BottomBorder) > 0.0) return true; if (borderStyle(TlbrBorder) != BorderNone && borderWidth(TlbrBorder) > 0.0) return true; if (borderStyle(BltrBorder) != BorderNone && borderWidth(BltrBorder) > 0.0) return true; return false; } bool KoBorder::hasBorder(KoBorder::BorderSide side) const { return borderStyle(side) != BorderNone && borderWidth(side) > 0.0; } // ---------------------------------------------------------------- // painting void KoBorder::paint(QPainter &painter, const QRectF &borderRect, BorderPaintArea whereToPaint) const { Q_UNUSED(whereToPaint); // In tables it is apparently best practice to paint the // horizontal lines over the vertical ones. So let's use the same // strategy here. QPointF start; QPointF end; // FIXME: Make KoBorder store pointers to BorderData instead. This is very inefficient. BorderData leftEdge = borderData(KoBorder::LeftBorder); BorderData rightEdge = borderData(KoBorder::RightBorder); BorderData topEdge = borderData(KoBorder::TopBorder); BorderData bottomEdge = borderData(KoBorder::BottomBorder); // Left border if (hasBorder(LeftBorder)) { start = borderRect.topLeft(); end = borderRect.bottomLeft(); paintBorderSide(painter, start, end, &leftEdge, true, hasBorder(TopBorder) ? &topEdge : 0, hasBorder(BottomBorder) ? &bottomEdge : 0, 1); } // Right border if (hasBorder(RightBorder)) { start = borderRect.topRight(); end = borderRect.bottomRight(); paintBorderSide(painter, start, end, &rightEdge, true, hasBorder(TopBorder) ? &topEdge : 0, hasBorder(BottomBorder) ? &bottomEdge : 0, -1); } // Top border if (hasBorder(TopBorder)) { start = borderRect.topLeft(); end = borderRect.topRight(); paintBorderSide(painter, start, end, &topEdge, false, hasBorder(LeftBorder) ? &leftEdge : 0, hasBorder(RightBorder) ? &rightEdge : 0, 1); } // Bottom border if (hasBorder(BottomBorder)) { start = borderRect.bottomLeft(); end = borderRect.bottomRight(); paintBorderSide(painter, start, end, &bottomEdge, false, hasBorder(LeftBorder) ? &leftEdge : 0, hasBorder(RightBorder) ? &rightEdge : 0, -1); } // FIXME: Diagonal borders } void KoBorder::paintBorderSide(QPainter &painter, QPointF lineStart, QPointF lineEnd, BorderData *borderData, bool isVertical, BorderData *neighbour1, BorderData *neighbour2, int inwardsAcross) const { // Adjust the outer line so that it is inside the boundary. qreal displacement = borderData->outerPen.widthF() / qreal(2.0); if (isVertical) { lineStart.setX(lineStart.x() + inwardsAcross * displacement); lineEnd.setX(lineEnd.x() + inwardsAcross * displacement); } else { lineStart.setY(lineStart.y() + inwardsAcross * displacement); lineEnd.setY(lineEnd.y() + inwardsAcross * displacement); } painter.setPen(borderData->outerPen); painter.drawLine(lineStart, lineEnd); if (borderData->style == BorderDouble) { displacement = (borderData->outerPen.widthF() / qreal(2.0) + borderData->spacing + borderData->innerPen.widthF() / qreal(2.0)); if (isVertical) { lineStart.setX(lineStart.x() + inwardsAcross * displacement); lineEnd.setX(lineEnd.x() + inwardsAcross * displacement); } else { lineStart.setY(lineStart.y() + inwardsAcross * displacement); lineEnd.setY(lineEnd.y() + inwardsAcross * displacement); } - // Adjust for neigboring inner lines. + // Adjust for neighboring inner lines. if (neighbour1 && neighbour1->style == BorderDouble) { displacement = neighbour1->outerPen.widthF() + neighbour1->spacing; if (isVertical) { lineStart.setY(lineStart.y() + displacement); } else { lineStart.setX(lineStart.x() + displacement); } } if (neighbour2 && neighbour2->style == BorderDouble) { displacement = neighbour2->outerPen.widthF() + neighbour2->spacing; if (isVertical) { lineEnd.setY(lineEnd.y() - displacement); } else { lineEnd.setX(lineEnd.x() - displacement); } } // Draw the inner line. painter.setPen(borderData->innerPen); painter.drawLine(lineStart, lineEnd); } } // ---------------------------------------------------------------- // static functions void parseOdfBorder(const QString &border, QColor *color, KoBorder::BorderStyle *borderStyle, bool *hasBorderStyle, qreal *borderWidth, bool *hasBorderWidth) { *hasBorderStyle = false; *hasBorderWidth = false; if (!border.isEmpty() && border != "none" && border != "hidden") { QStringList borderData = border.split(' ', QString::SkipEmptyParts); if (borderData.length() > 0) { const QColor borderColor = QColor(borderData.last()); if (borderColor.isValid()) { *color = borderColor; borderData.removeLast(); } bool converted = false; const KoBorder::BorderStyle parsedBorderStyle = KoBorder::odfBorderStyle(borderData.last(), &converted); if (converted) { *hasBorderStyle = true; borderData.removeLast(); *borderStyle = parsedBorderStyle; } if (!borderData.isEmpty()) { const qreal parsedBorderWidth = KoUnit::parseValue(borderData[0], 1.0); *borderWidth = parsedBorderWidth; *hasBorderWidth = true; } } } } // ---------------------------------------------------------------- // load and save bool KoBorder::loadOdf(const KoXmlElement &style) { bool result = false; QString borderString; bool hasSpecialBorder; QString specialBorderString; if (style.hasAttributeNS(KoXmlNS::fo, "border")) { borderString = style.attributeNS(KoXmlNS::fo, "border"); if (borderString == "none") { // We use the "false" to indicate that there is no border // rather than that the parsing has failed. return false; } result = true; if ((hasSpecialBorder = style.hasAttributeNS(KoXmlNS::calligra, "specialborder"))) { specialBorderString = style.attributeNS(KoXmlNS::calligra, "specialborder"); } parseAndSetBorder(borderString, hasSpecialBorder, specialBorderString); } else { // No common border attributes, check for the individual ones. if (style.hasAttributeNS(KoXmlNS::fo, "border-left")) { result = true; borderString = style.attributeNS(KoXmlNS::fo, "border-left"); if ((hasSpecialBorder = style.hasAttributeNS(KoXmlNS::calligra, "specialborder-left"))) { specialBorderString = style.attributeNS(KoXmlNS::calligra, "specialborder-left"); } parseAndSetBorder(LeftBorder, borderString, hasSpecialBorder, specialBorderString); } if (style.hasAttributeNS(KoXmlNS::fo, "border-top")) { result = true; borderString = style.attributeNS(KoXmlNS::fo, "border-top"); if ((hasSpecialBorder = style.hasAttributeNS(KoXmlNS::calligra, "specialborder-top"))) { specialBorderString = style.attributeNS(KoXmlNS::calligra, "specialborder-top"); } parseAndSetBorder(TopBorder, borderString, hasSpecialBorder, specialBorderString); } if (style.hasAttributeNS(KoXmlNS::fo, "border-right")) { result = true; borderString = style.attributeNS(KoXmlNS::fo, "border-right"); if ((hasSpecialBorder = style.hasAttributeNS(KoXmlNS::calligra, "specialborder-right"))) { specialBorderString = style.attributeNS(KoXmlNS::calligra, "specialborder-right"); } parseAndSetBorder(RightBorder, borderString, hasSpecialBorder, specialBorderString); } if (style.hasAttributeNS(KoXmlNS::fo, "border-bottom")) { result = true; borderString = style.attributeNS(KoXmlNS::fo, "border-bottom"); if ((hasSpecialBorder = style.hasAttributeNS(KoXmlNS::calligra, "specialborder-bottom"))) { specialBorderString = style.attributeNS(KoXmlNS::calligra, "specialborder-bottom"); } parseAndSetBorder(BottomBorder, borderString, hasSpecialBorder, specialBorderString); } } // Diagonals are treated individually and are NOT part of . if (style.hasAttributeNS(KoXmlNS::style, "diagonal-tl-br")) { result = true; borderString = style.attributeNS(KoXmlNS::fo, "border-tl-br"); if ((hasSpecialBorder = style.hasAttributeNS(KoXmlNS::calligra, "specialborder-tl-br"))) { specialBorderString = style.attributeNS(KoXmlNS::calligra, "specialborder-tl-br"); } parseAndSetBorder(TlbrBorder, borderString, hasSpecialBorder, specialBorderString); } if (style.hasAttributeNS(KoXmlNS::style, "diagonal-bl-tr")) { result = true; borderString = style.attributeNS(KoXmlNS::fo, "border-bl-tr"); if ((hasSpecialBorder = style.hasAttributeNS(KoXmlNS::calligra, "specialborder-bl-tr"))) { specialBorderString = style.attributeNS(KoXmlNS::calligra, "specialborder-bl-tr"); } parseAndSetBorder(BltrBorder, borderString, hasSpecialBorder, specialBorderString); } // Handle double borders. if (style.hasAttributeNS(KoXmlNS::style, "border-line-width")) { result = true; QString borderLineWidth = style.attributeNS(KoXmlNS::style, "border-line-width"); if (!borderLineWidth.isEmpty() && borderLineWidth != "none" && borderLineWidth != "hidden") { QStringList blw = borderLineWidth.split(' ', QString::SkipEmptyParts); setInnerBorderWidth(LeftBorder, KoUnit::parseValue(blw[0], 0.1)); setBorderSpacing(LeftBorder, KoUnit::parseValue(blw[1], 1.0)); setBorderWidth(LeftBorder, KoUnit::parseValue(blw[2], 0.1)); setInnerBorderWidth(TopBorder, KoUnit::parseValue(blw[0], 0.1)); setBorderSpacing(TopBorder, KoUnit::parseValue(blw[1], 1.0)); setBorderWidth(TopBorder, KoUnit::parseValue(blw[2], 0.1)); setInnerBorderWidth(RightBorder, KoUnit::parseValue(blw[0], 0.1)); setBorderSpacing(RightBorder, KoUnit::parseValue(blw[1], 1.0)); setBorderWidth(RightBorder, KoUnit::parseValue(blw[2], 0.1)); setInnerBorderWidth(BottomBorder, KoUnit::parseValue(blw[0], 0.1)); setBorderSpacing(BottomBorder, KoUnit::parseValue(blw[1], 1.0)); setBorderWidth(BottomBorder, KoUnit::parseValue(blw[2], 0.1)); } } else { if (style.hasAttributeNS(KoXmlNS::style, "border-line-width-left")) { result = true; QString borderLineWidth = style.attributeNS(KoXmlNS::style, "border-line-width-left"); if (!borderLineWidth.isEmpty() && borderLineWidth != "none" && borderLineWidth != "hidden") { QStringList blw = borderLineWidth.split(' ', QString::SkipEmptyParts); setInnerBorderWidth(LeftBorder, KoUnit::parseValue(blw[0], 0.1)); setBorderSpacing(LeftBorder, KoUnit::parseValue(blw[1], 1.0)); setBorderWidth(LeftBorder, KoUnit::parseValue(blw[2], 0.1)); } } if (style.hasAttributeNS(KoXmlNS::style, "border-line-width-top")) { result = true; QString borderLineWidth = style.attributeNS(KoXmlNS::style, "border-line-width-top"); if (!borderLineWidth.isEmpty() && borderLineWidth != "none" && borderLineWidth != "hidden") { QStringList blw = borderLineWidth.split(' ', QString::SkipEmptyParts); setInnerBorderWidth(TopBorder, KoUnit::parseValue(blw[0], 0.1)); setBorderSpacing(TopBorder, KoUnit::parseValue(blw[1], 1.0)); setBorderWidth(TopBorder, KoUnit::parseValue(blw[2], 0.1)); } } if (style.hasAttributeNS(KoXmlNS::style, "border-line-width-right")) { result = true; QString borderLineWidth = style.attributeNS(KoXmlNS::style, "border-line-width-right"); if (!borderLineWidth.isEmpty() && borderLineWidth != "none" && borderLineWidth != "hidden") { QStringList blw = borderLineWidth.split(' ', QString::SkipEmptyParts); setInnerBorderWidth(RightBorder, KoUnit::parseValue(blw[0], 0.1)); setBorderSpacing(RightBorder, KoUnit::parseValue(blw[1], 1.0)); setBorderWidth(RightBorder, KoUnit::parseValue(blw[2], 0.1)); } } if (style.hasAttributeNS(KoXmlNS::style, "border-line-width-bottom")) { result = true; QString borderLineWidth = style.attributeNS(KoXmlNS::style, "border-line-width-bottom"); if (!borderLineWidth.isEmpty() && borderLineWidth != "none" && borderLineWidth != "hidden") { QStringList blw = borderLineWidth.split(' ', QString::SkipEmptyParts); setInnerBorderWidth(BottomBorder, KoUnit::parseValue(blw[0], 0.1)); setBorderSpacing(BottomBorder, KoUnit::parseValue(blw[1], 1.0)); setBorderWidth(BottomBorder, KoUnit::parseValue(blw[2], 0.1)); } } } if (style.hasAttributeNS(KoXmlNS::style, "diagonal-tl-br-widths")) { result = true; QString borderLineWidth = style.attributeNS(KoXmlNS::style, "diagonal-tl-br-widths"); if (!borderLineWidth.isEmpty() && borderLineWidth != "none" && borderLineWidth != "hidden") { QStringList blw = borderLineWidth.split(' ', QString::SkipEmptyParts); setInnerBorderWidth(TlbrBorder, KoUnit::parseValue(blw[0], 0.1)); setBorderSpacing(TlbrBorder, KoUnit::parseValue(blw[1], 1.0)); setBorderWidth(TlbrBorder, KoUnit::parseValue(blw[2], 0.1)); } } if (style.hasAttributeNS(KoXmlNS::style, "diagonal-bl-tr-widths")) { result = true; QString borderLineWidth = style.attributeNS(KoXmlNS::style, "diagonal-bl-tr-widths"); if (!borderLineWidth.isEmpty() && borderLineWidth != "none" && borderLineWidth != "hidden") { QStringList blw = borderLineWidth.split(' ', QString::SkipEmptyParts); setInnerBorderWidth(BltrBorder, KoUnit::parseValue(blw[0], 0.1)); setBorderSpacing(BltrBorder, KoUnit::parseValue(blw[1], 1.0)); setBorderWidth(BltrBorder, KoUnit::parseValue(blw[2], 0.1)); } } return result; } bool KoBorder::loadOdf(const KoStyleStack &styleStack) { bool result = false; QString borderString; bool hasSpecialBorder; QString specialBorderString; if (styleStack.hasProperty(KoXmlNS::fo, "border")) { result = true; borderString = styleStack.property(KoXmlNS::fo, "border"); if ((hasSpecialBorder = styleStack.hasProperty(KoXmlNS::calligra, "specialborder"))) { specialBorderString = styleStack.property(KoXmlNS::calligra, "specialborder"); } parseAndSetBorder(borderString, hasSpecialBorder, specialBorderString); } // Even if there are common border attributes, check for the // individual ones since they have precedence. if (styleStack.hasProperty(KoXmlNS::fo, "border-left")) { result = true; borderString = styleStack.property(KoXmlNS::fo, "border-left"); if ((hasSpecialBorder = styleStack.hasProperty(KoXmlNS::calligra, "specialborder-left"))) { specialBorderString = styleStack.property(KoXmlNS::calligra, "specialborder-left"); } parseAndSetBorder(LeftBorder, borderString, hasSpecialBorder, specialBorderString); } if (styleStack.hasProperty(KoXmlNS::fo, "border-top")) { result = true; borderString = styleStack.property(KoXmlNS::fo, "border-top"); if ((hasSpecialBorder = styleStack.hasProperty(KoXmlNS::calligra, "specialborder-top"))) { specialBorderString = styleStack.property(KoXmlNS::calligra, "specialborder-top"); } parseAndSetBorder(TopBorder, borderString, hasSpecialBorder, specialBorderString); } if (styleStack.hasProperty(KoXmlNS::fo, "border-right")) { result = true; borderString = styleStack.property(KoXmlNS::fo, "border-right"); if ((hasSpecialBorder = styleStack.hasProperty(KoXmlNS::calligra, "specialborder-right"))) { specialBorderString = styleStack.property(KoXmlNS::calligra, "specialborder-right"); } parseAndSetBorder(RightBorder, borderString, hasSpecialBorder, specialBorderString); } if (styleStack.hasProperty(KoXmlNS::fo, "border-bottom")) { result = true; borderString = styleStack.property(KoXmlNS::fo, "border-bottom"); if ((hasSpecialBorder = styleStack.hasProperty(KoXmlNS::calligra, "specialborder-bottom"))) { specialBorderString = styleStack.property(KoXmlNS::calligra, "specialborder-bottom"); } parseAndSetBorder(BottomBorder, borderString, hasSpecialBorder, specialBorderString); } // Diagonals are treated individually and are NOT part of . if (styleStack.hasProperty(KoXmlNS::style, "diagonal-tl-br")) { result = true; borderString = styleStack.property(KoXmlNS::fo, "border-tl-br"); if ((hasSpecialBorder = styleStack.hasProperty(KoXmlNS::calligra, "specialborder-tl-br"))) { specialBorderString = styleStack.property(KoXmlNS::calligra, "specialborder-tl-br"); } parseAndSetBorder(TlbrBorder, borderString, hasSpecialBorder, specialBorderString); } if (styleStack.hasProperty(KoXmlNS::style, "diagonal-bl-tr")) { result = true; borderString = styleStack.property(KoXmlNS::fo, "border-bl-tr"); if ((hasSpecialBorder = styleStack.hasProperty(KoXmlNS::calligra, "specialborder-bl-tr"))) { specialBorderString = styleStack.property(KoXmlNS::calligra, "specialborder-bl-tr"); } parseAndSetBorder(BltrBorder, borderString, hasSpecialBorder, specialBorderString); } // Handle double borders. if (styleStack.hasProperty(KoXmlNS::style, "border-line-width")) { result = true; QString borderLineWidth = styleStack.property(KoXmlNS::style, "border-line-width"); if (!borderLineWidth.isEmpty() && borderLineWidth != "none" && borderLineWidth != "hidden") { QStringList blw = borderLineWidth.split(' ', QString::SkipEmptyParts); setInnerBorderWidth(LeftBorder, KoUnit::parseValue(blw[0], 0.1)); setBorderSpacing(LeftBorder, KoUnit::parseValue(blw[1], 1.0)); setBorderWidth(LeftBorder, KoUnit::parseValue(blw[2], 0.1)); setInnerBorderWidth(TopBorder, KoUnit::parseValue(blw[0], 0.1)); setBorderSpacing(TopBorder, KoUnit::parseValue(blw[1], 1.0)); setBorderWidth(TopBorder, KoUnit::parseValue(blw[2], 0.1)); setInnerBorderWidth(RightBorder, KoUnit::parseValue(blw[0], 0.1)); setBorderSpacing(RightBorder, KoUnit::parseValue(blw[1], 1.0)); setBorderWidth(RightBorder, KoUnit::parseValue(blw[2], 0.1)); setInnerBorderWidth(BottomBorder, KoUnit::parseValue(blw[0], 0.1)); setBorderSpacing(BottomBorder, KoUnit::parseValue(blw[1], 1.0)); setBorderWidth(BottomBorder, KoUnit::parseValue(blw[2], 0.1)); } } // Even if there are common border attributes, check for the // individual ones since they have precedence. if (styleStack.hasProperty(KoXmlNS::style, "border-line-width-left")) { result = true; QString borderLineWidth = styleStack.property(KoXmlNS::style, "border-line-width-left"); if (!borderLineWidth.isEmpty() && borderLineWidth != "none" && borderLineWidth != "hidden") { QStringList blw = borderLineWidth.split(' ', QString::SkipEmptyParts); setInnerBorderWidth(LeftBorder, KoUnit::parseValue(blw[0], 0.1)); setBorderSpacing(LeftBorder, KoUnit::parseValue(blw[1], 1.0)); setBorderWidth(LeftBorder, KoUnit::parseValue(blw[2], 0.1)); } } if (styleStack.hasProperty(KoXmlNS::style, "border-line-width-top")) { result = true; QString borderLineWidth = styleStack.property(KoXmlNS::style, "border-line-width-top"); if (!borderLineWidth.isEmpty() && borderLineWidth != "none" && borderLineWidth != "hidden") { QStringList blw = borderLineWidth.split(' ', QString::SkipEmptyParts); setInnerBorderWidth(TopBorder, KoUnit::parseValue(blw[0], 0.1)); setBorderSpacing(TopBorder, KoUnit::parseValue(blw[1], 1.0)); setBorderWidth(TopBorder, KoUnit::parseValue(blw[2], 0.1)); } } if (styleStack.hasProperty(KoXmlNS::style, "border-line-width-right")) { result = true; QString borderLineWidth = styleStack.property(KoXmlNS::style, "border-line-width-right"); if (!borderLineWidth.isEmpty() && borderLineWidth != "none" && borderLineWidth != "hidden") { QStringList blw = borderLineWidth.split(' ', QString::SkipEmptyParts); setInnerBorderWidth(RightBorder, KoUnit::parseValue(blw[0], 0.1)); setBorderSpacing(RightBorder, KoUnit::parseValue(blw[1], 1.0)); setBorderWidth(RightBorder, KoUnit::parseValue(blw[2], 0.1)); } } if (styleStack.hasProperty(KoXmlNS::style, "border-line-width-bottom")) { result = true; QString borderLineWidth = styleStack.property(KoXmlNS::style, "border-line-width-bottom"); if (!borderLineWidth.isEmpty() && borderLineWidth != "none" && borderLineWidth != "hidden") { QStringList blw = borderLineWidth.split(' ', QString::SkipEmptyParts); setInnerBorderWidth(BottomBorder, KoUnit::parseValue(blw[0], 0.1)); setBorderSpacing(BottomBorder, KoUnit::parseValue(blw[1], 1.0)); setBorderWidth(BottomBorder, KoUnit::parseValue(blw[2], 0.1)); } } // Diagonals are treated individually and are NOT part of . if (styleStack.hasProperty(KoXmlNS::style, "diagonal-tl-br-widths")) { result = true; QString borderLineWidth = styleStack.property(KoXmlNS::style, "diagonal-tl-br-widths"); if (!borderLineWidth.isEmpty() && borderLineWidth != "none" && borderLineWidth != "hidden") { QStringList blw = borderLineWidth.split(' ', QString::SkipEmptyParts); setInnerBorderWidth(TlbrBorder, KoUnit::parseValue(blw[0], 0.1)); setBorderSpacing(TlbrBorder, KoUnit::parseValue(blw[1], 1.0)); setBorderWidth(TlbrBorder, KoUnit::parseValue(blw[2], 0.1)); } } if (styleStack.hasProperty(KoXmlNS::style, "diagonal-bl-tr-widths")) { result = true; QString borderLineWidth = styleStack.property(KoXmlNS::style, "diagonal-bl-tr-widths"); if (!borderLineWidth.isEmpty() && borderLineWidth != "none" && borderLineWidth != "hidden") { QStringList blw = borderLineWidth.split(' ', QString::SkipEmptyParts); setInnerBorderWidth(BltrBorder, KoUnit::parseValue(blw[0], 0.1)); setBorderSpacing(BltrBorder, KoUnit::parseValue(blw[1], 1.0)); setBorderWidth(BltrBorder, KoUnit::parseValue(blw[2], 0.1)); } } return result; } // Private void KoBorder::parseAndSetBorder(const QString &borderString, bool hasSpecialBorder, const QString &specialBorderString) { if (borderString == "none") { return; } //debugOdf << "*** *** Found border: " << border; QColor bordersColor; BorderStyle bordersStyle; qreal bordersWidth; bool foundStyle; bool foundWidth; parseOdfBorder(borderString, &bordersColor, &bordersStyle, &foundStyle, &bordersWidth, &foundWidth); if (bordersColor.isValid()) { setBorderColor(LeftBorder, bordersColor); setBorderColor(TopBorder, bordersColor); setBorderColor(RightBorder, bordersColor); setBorderColor(BottomBorder, bordersColor); } if (hasSpecialBorder) { bordersStyle = KoBorder::odfBorderStyle(specialBorderString, &foundStyle); } if (foundStyle) { setBorderStyle(LeftBorder, bordersStyle); setBorderStyle(TopBorder, bordersStyle); setBorderStyle(RightBorder, bordersStyle); setBorderStyle(BottomBorder, bordersStyle); } if (foundWidth) { setBorderWidth(LeftBorder, bordersWidth); setBorderWidth(TopBorder, bordersWidth); setBorderWidth(RightBorder, bordersWidth); setBorderWidth(BottomBorder, bordersWidth); } } // Private void KoBorder::parseAndSetBorder(const BorderSide borderSide, const QString &borderString, bool hasSpecialBorder, const QString &specialBorderString) { QColor borderColor; BorderStyle borderStyle; qreal borderWidth; bool foundStyle; bool foundWidth; parseOdfBorder(borderString, &borderColor, &borderStyle, &foundStyle, &borderWidth, &foundWidth); if (borderColor.isValid()) { setBorderColor(borderSide, borderColor); } if (hasSpecialBorder) { borderStyle = KoBorder::odfBorderStyle(specialBorderString, &foundStyle); } if (foundStyle) { setBorderStyle( borderSide, borderStyle); } if (foundWidth) { setBorderWidth( borderSide, borderWidth); } } void KoBorder::saveOdf(KoGenStyle &style, KoGenStyle::PropertyType type) const { // Get the strings that describe respective borders. QString leftBorderString = QString("%1pt %2 %3") .arg(QString::number(borderWidth(LeftBorder)), odfBorderStyleString(borderStyle(LeftBorder)), borderColor(LeftBorder).name()); QString rightBorderString = QString("%1pt %2 %3") .arg(QString::number(borderWidth(RightBorder)), odfBorderStyleString(borderStyle(RightBorder)), borderColor(RightBorder).name()); QString topBorderString = QString("%1pt %2 %3") .arg(QString::number(borderWidth(TopBorder)), odfBorderStyleString(borderStyle(TopBorder)), borderColor(TopBorder).name()); QString bottomBorderString = QString("%1pt %2 %3") .arg(QString::number(borderWidth(BottomBorder)), odfBorderStyleString(borderStyle(BottomBorder)), borderColor(BottomBorder).name()); QString tlbrBorderString = QString("%1pt %2 %3") .arg(QString::number(borderWidth(TlbrBorder)), odfBorderStyleString(borderStyle(TlbrBorder)), borderColor(TlbrBorder).name()); QString trblBorderString = QString("%1pt %2 %3") .arg(QString::number(borderWidth(BltrBorder)), odfBorderStyleString(borderStyle(BltrBorder)), borderColor(BltrBorder).name()); // Get the strings that describe respective special borders (for special mso support). QString leftBorderSpecialString = msoBorderStyleString(borderStyle(LeftBorder)); QString rightBorderSpecialString = msoBorderStyleString(borderStyle(RightBorder)); QString topBorderSpecialString = msoBorderStyleString(borderStyle(TopBorder)); QString bottomBorderSpecialString = msoBorderStyleString(borderStyle(BottomBorder)); QString tlbrBorderSpecialString = msoBorderStyleString(borderStyle(TlbrBorder)); QString trblBorderSpecialString = msoBorderStyleString(borderStyle(BltrBorder)); // Check if we can save all borders in one fo:border attribute, or // if we have to use several different ones like fo:border-left, etc. if (leftBorderString == rightBorderString && leftBorderString == topBorderString && leftBorderString == bottomBorderString) { // Yes, they were all the same, so use only fo:border style.addProperty("fo:border", leftBorderString, type); style.addProperty("calligra:specialborder-left", leftBorderSpecialString, type); style.addProperty("calligra:specialborder-right", rightBorderSpecialString, type); style.addProperty("calligra:specialborder-top", topBorderSpecialString, type); style.addProperty("calligra:specialborder-bottom", bottomBorderSpecialString, type); } else { // No, they were different, so use the individual borders. //if (leftBorderStyle() != BorderNone) style.addProperty("fo:border-left", leftBorderString, type); style.addProperty("calligra:specialborder-left", leftBorderSpecialString, type); //if (rightBorderStyle() != BorderNone) style.addProperty("fo:border-right", rightBorderString, type); style.addProperty("calligra:specialborder-right", rightBorderSpecialString, type); //if (topBorderStyle() != BorderNone) style.addProperty("fo:border-top", topBorderString, type); style.addProperty("calligra:specialborder-top", topBorderSpecialString, type); //if (bottomBorderStyle() != BorderNone) style.addProperty("fo:border-bottom", bottomBorderString, type); style.addProperty("calligra:specialborder-bottom", bottomBorderSpecialString, type); } if (style.type() != KoGenStyle::PageLayoutStyle) { //if (tlbrBorderStyle() != BorderNone) { style.addProperty("style:diagonal-tl-br", tlbrBorderString, type); //} //if (trblBorderStyle() != BorderNone) { style.addProperty("style:diagonal-bl-tr", trblBorderString, type); //} } // Handle double borders QString leftBorderLineWidth = QString("%1pt %2pt %3pt") .arg(QString::number(innerBorderWidth(LeftBorder)), QString::number(borderSpacing(LeftBorder)), QString::number(borderWidth(LeftBorder))); QString rightBorderLineWidth = QString("%1pt %2pt %3pt") .arg(QString::number(innerBorderWidth(RightBorder)), QString::number(borderSpacing(RightBorder)), QString::number(borderWidth(RightBorder))); QString topBorderLineWidth = QString("%1pt %2pt %3pt") .arg(QString::number(innerBorderWidth(TopBorder)), QString::number(borderSpacing(TopBorder)), QString::number(borderWidth(TopBorder))); QString bottomBorderLineWidth = QString("%1pt %2pt %3pt") .arg(QString::number(innerBorderWidth(BottomBorder)), QString::number(borderSpacing(BottomBorder)), QString::number(borderWidth(BottomBorder))); QString tlbrBorderLineWidth = QString("%1pt %2pt %3pt") .arg(QString::number(innerBorderWidth(TlbrBorder)), QString::number(borderSpacing(TlbrBorder)), QString::number(borderWidth(TlbrBorder))); QString trblBorderLineWidth = QString("%1pt %2pt %3pt") .arg(QString::number(innerBorderWidth(BltrBorder)), QString::number(borderSpacing(BltrBorder)), QString::number(borderWidth(BltrBorder))); if (leftBorderLineWidth == rightBorderLineWidth && leftBorderLineWidth == topBorderLineWidth && leftBorderLineWidth == bottomBorderLineWidth && borderStyle(LeftBorder) == borderStyle(RightBorder) && borderStyle(TopBorder) == borderStyle(BottomBorder) && borderStyle(TopBorder) == borderStyle(LeftBorder) && (borderStyle(LeftBorder) == BorderDouble || borderStyle(LeftBorder) == BorderDoubleWave)) { style.addProperty("style:border-line-width", leftBorderLineWidth, type); } else { if (borderStyle(LeftBorder) == BorderDouble || borderStyle(LeftBorder) == BorderDoubleWave) style.addProperty("style:border-line-width-left", leftBorderLineWidth, type); if (borderStyle(RightBorder) == BorderDouble || borderStyle(RightBorder) == BorderDoubleWave) style.addProperty("style:border-line-width-right", rightBorderLineWidth, type); if (borderStyle(TopBorder) == BorderDouble || borderStyle(TopBorder) == BorderDoubleWave) style.addProperty("style:border-line-width-top", topBorderLineWidth, type); if (borderStyle(BottomBorder) == BorderDouble || borderStyle(BottomBorder) == BorderDoubleWave) style.addProperty("style:border-line-width-bottom", bottomBorderLineWidth, type); } if (style.type() != KoGenStyle::PageLayoutStyle) { if (borderStyle(TlbrBorder) == BorderDouble || borderStyle(TlbrBorder) == BorderDoubleWave) { style.addProperty("style:diagonal-tl-br-widths", tlbrBorderLineWidth, type); } if (borderStyle(BltrBorder) == BorderDouble || borderStyle(BltrBorder) == BorderDoubleWave) { style.addProperty("style:diagonal-bl-tr-widths", trblBorderLineWidth, type); } } } diff --git a/libs/odf/KoOdfBibliographyConfiguration.h b/libs/odf/KoOdfBibliographyConfiguration.h index a5e92e9c18..d7dca92020 100644 --- a/libs/odf/KoOdfBibliographyConfiguration.h +++ b/libs/odf/KoOdfBibliographyConfiguration.h @@ -1,116 +1,116 @@ /* This file is part of the KDE project * Copyright (C) 2011 Smit Patel * * 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 KOODFBIBLIOGRAPHYCONFIGURATION_H #define KOODFBIBLIOGRAPHYCONFIGURATION_H #include #include #include #include "KoXmlReaderForward.h" #include "kritaodf_export.h" class KoXmlWriter; typedef QPair SortKeyPair; /** * load and save the bibliography-configuration element from the text: namespace. * • Prefix * • Suffix * • Numbered entries * • Sort by position * • Sort algorithm */ class KRITAODF_EXPORT KoOdfBibliographyConfiguration : public QObject { Q_OBJECT public: KoOdfBibliographyConfiguration(); ~KoOdfBibliographyConfiguration() override; KoOdfBibliographyConfiguration(const KoOdfBibliographyConfiguration &other); KoOdfBibliographyConfiguration &operator=(const KoOdfBibliographyConfiguration &other); static const QList bibTypes; static const QList bibDataFields; /** * load the bibliography-configuration element */ void loadOdf(const KoXmlElement &element); /** * save the bibliography-configuration element */ void saveOdf(KoXmlWriter * writer) const; /** * Sort by position * If text:sort-by-position attribute is true then bibliography entries or citations will be * sorted according to their position in document. */ bool sortByPosition() const; void setSortByPosition(bool enable); /** - * Numberred entries + * Numbered entries * If text:numbered-entries attribute is true then bibliography entries or citations will be numbered. */ bool numberedEntries() const; void setNumberedEntries(bool enable); /** * Prefix * The text:prefix attribute specifies prefix of bibliography entry or citation(text:bibliography-mark) */ QString prefix() const; void setPrefix(const QString &prefixValue); /** * Suffix * The text:suffix attribute specifies suffix of bibliography entry or citation(text:bibliography-mark) */ QString suffix() const; void setSuffix(const QString &suffixValue); /** * Sort algorithm * The text:sort-algorithm attribute specifies sorting algorithm for bibliography entry */ QString sortAlgorithm() const; void setSortAlgorithm(const QString &algorithm); /** * Sort Keys * The text:sort-key attribute specifies sort key for bibliography entry */ QList sortKeys() const; void setSortKeys(const QList &sortKeys); private: class Private; Private * const d; }; Q_DECLARE_METATYPE(KoOdfBibliographyConfiguration*) #endif // KOODFBIBLIOGRAPHYCONFIGURATION_H diff --git a/libs/pigment/KoBasicHistogramProducers.h b/libs/pigment/KoBasicHistogramProducers.h index 3f695ccdb7..5509176705 100644 --- a/libs/pigment/KoBasicHistogramProducers.h +++ b/libs/pigment/KoBasicHistogramProducers.h @@ -1,267 +1,267 @@ /* * Copyright (c) 2005 Bart Coppens * * 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. */ #ifndef _Ko_BASIC_HISTOGRAM_PRODUCERS_ #define _Ko_BASIC_HISTOGRAM_PRODUCERS_ #include "KoHistogramProducer.h" #include #include #include "KoColorSpace.h" #include "KoID.h" #include "kritapigment_export.h" #include "KoColorSpaceRegistry.h" class KRITAPIGMENT_EXPORT KoBasicHistogramProducer : public KoHistogramProducer { public: explicit KoBasicHistogramProducer(const KoID& id, int channelCount, int nrOfBins); explicit KoBasicHistogramProducer(const KoID& id, int nrOfBins, const KoColorSpace *colorSpace); ~KoBasicHistogramProducer() override {} void clear() override; void setView(qreal from, qreal size) override { m_from = from; m_width = size; } const KoID& id() const override { return m_id; } QList channels() override { return m_colorSpace->channels(); } qint32 numberOfBins() override { return m_nrOfBins; } qreal viewFrom() const override { return m_from; } qreal viewWidth() const override { return m_width; } qint32 count() override { return m_count; } qint32 getBinAt(int channel, int position) override { return m_bins.at(externalToInternal(channel)).at(position); } qint32 outOfViewLeft(int channel) override { return m_outLeft.at(externalToInternal(channel)); } qint32 outOfViewRight(int channel) override { return m_outRight.at(externalToInternal(channel)); } protected: /** * The order in which channels() returns is not the same as the internal representation, * that of the pixel internally. This method converts external usage to internal usage. - * This method uses some basic assumtpions about the layout of the pixel, so _extremely_ + * This method uses some basic assumptions about the layout of the pixel, so _extremely_ * exotic spaces might want to override this (see makeExternalToInternal source for * those assumptions) **/ virtual int externalToInternal(int ext) { if (channels().count() > 0 && m_external.count() == 0) // Set up the translation table makeExternalToInternal(); return m_external.at(ext); } // not virtual since that is useless: we call it from constructor void makeExternalToInternal(); typedef QVector vBins; QVector m_bins; vBins m_outLeft, m_outRight; qreal m_from, m_width; qint32 m_count; int m_channels, m_nrOfBins; const KoColorSpace *m_colorSpace; KoID m_id; QVector m_external; }; class KRITAPIGMENT_EXPORT KoBasicU8HistogramProducer : public KoBasicHistogramProducer { public: KoBasicU8HistogramProducer(const KoID& id, const KoColorSpace *colorSpace); ~KoBasicU8HistogramProducer() override {} void addRegionToBin(const quint8 * pixels, const quint8 * selectionMask, quint32 nPixels, const KoColorSpace *colorSpace) override; QString positionToString(qreal pos) const override; qreal maximalZoom() const override { return 1.0; } }; class KRITAPIGMENT_EXPORT KoBasicU16HistogramProducer : public KoBasicHistogramProducer { public: KoBasicU16HistogramProducer(const KoID& id, const KoColorSpace *colorSpace); ~KoBasicU16HistogramProducer() override {} void addRegionToBin(const quint8 * pixels, const quint8 * selectionMask, quint32 nPixels, const KoColorSpace *colorSpace) override; QString positionToString(qreal pos) const override; qreal maximalZoom() const override; }; class KRITAPIGMENT_EXPORT KoBasicF32HistogramProducer : public KoBasicHistogramProducer { public: KoBasicF32HistogramProducer(const KoID& id, const KoColorSpace *colorSpace); ~KoBasicF32HistogramProducer() override {} void addRegionToBin(const quint8 * pixels, const quint8 * selectionMask, quint32 nPixels, const KoColorSpace *colorSpace) override; QString positionToString(qreal pos) const override; qreal maximalZoom() const override; }; #ifdef HAVE_OPENEXR class KRITAPIGMENT_EXPORT KoBasicF16HalfHistogramProducer : public KoBasicHistogramProducer { public: KoBasicF16HalfHistogramProducer(const KoID& id, const KoColorSpace *colorSpace); ~KoBasicF16HalfHistogramProducer() override {} void addRegionToBin(const quint8 * pixels, const quint8 * selectionMask, quint32 nPixels, const KoColorSpace *colorSpace) override; QString positionToString(qreal pos) const override; qreal maximalZoom() const override; }; #endif /** * Parametrized on a specific KoHistogramProducer. Its generated producers * will have the same KoID as the factory's. This is acceptable because we can't mix * Factories with Producers in the code because they are incompatible types, and * in the GUI we actually only need a producer's name, not a factory's. */ template class KoBasicHistogramProducerFactory : public KoHistogramProducerFactory { public: KoBasicHistogramProducerFactory(const KoID& id, const QString& modelId, const QString& depthId ) : KoHistogramProducerFactory(id), m_modelId(modelId), m_depthId(depthId) { } ~KoBasicHistogramProducerFactory() override {} KoHistogramProducer *generate() override { KoHistogramProducer *producer = 0; const KoColorSpace *cs = KoColorSpaceRegistry::instance()->colorSpace(m_modelId, m_depthId, 0); if (cs) { producer = new T(KoID(id(), name()), cs); } return producer; } bool isCompatibleWith(const KoColorSpace* colorSpace, bool strict = false) const override { if( strict ){ return colorSpace->colorDepthId().id() == m_depthId; } return colorSpace->colorModelId().id() == m_modelId || colorSpace->colorDepthId().id() == m_depthId; } float preferrednessLevelWith(const KoColorSpace* colorSpace) const override { return 0.5 * ( (colorSpace->colorModelId().id() == m_modelId) + (colorSpace->colorDepthId().id() == m_depthId) ); } protected: QString m_modelId, m_depthId; }; /** * This is a Producer (with associated factory) that converts the pixels of the colorspace * to RGB8 with toQColor, and then does its counting on RGB. This is NOT registered with the * Registry, because it isCompatibleWith all colorspaces, and should only be used in extreme * cases (like no other producer being available **/ class KRITAPIGMENT_EXPORT KoGenericRGBHistogramProducer : public KoBasicHistogramProducer { public: KoGenericRGBHistogramProducer(); ~KoGenericRGBHistogramProducer() override {} void addRegionToBin(const quint8 * pixels, const quint8 * selectionMask, quint32 nPixels, const KoColorSpace *colorSpace) override; QString positionToString(qreal pos) const override; qreal maximalZoom() const override; QList channels() override; protected: QList m_channelsList; }; /** KoGenericRGBHistogramProducer his special Factory that isCompatibleWith everything. */ class KRITAPIGMENT_EXPORT KoGenericRGBHistogramProducerFactory : public KoHistogramProducerFactory { public: KoGenericRGBHistogramProducerFactory(); ~KoGenericRGBHistogramProducerFactory() override {} KoHistogramProducer *generate() override { return new KoGenericRGBHistogramProducer(); } bool isCompatibleWith(const KoColorSpace*, bool strict = false) const override { Q_UNUSED(strict); return true; } float preferrednessLevelWith(const KoColorSpace*) const override { return 0.0; } }; /** * This is a Producer (with associated factory) that converts the pixels of the colorspace * to L*a*b*, and then does its counting. * It isCompatibleWith all colorspaces **/ class KRITAPIGMENT_EXPORT KoGenericLabHistogramProducer : public KoBasicHistogramProducer { public: KoGenericLabHistogramProducer(); ~KoGenericLabHistogramProducer() override; void addRegionToBin(const quint8 * pixels, const quint8 * selectionMask, quint32 nPixels, const KoColorSpace *colorSpace) override; QString positionToString(qreal pos) const override; qreal maximalZoom() const override; QList channels() override; protected: QList m_channelsList; }; /** KoGenericLabHistogramProducer his special Factory that isCompatibleWith everything. */ class /*KRITAPIGMENT_EXPORT*/ KoGenericLabHistogramProducerFactory : public KoHistogramProducerFactory { public: KoGenericLabHistogramProducerFactory(); ~KoGenericLabHistogramProducerFactory() override {} KoHistogramProducer *generate() override { return new KoGenericLabHistogramProducer(); } bool isCompatibleWith(const KoColorSpace*, bool strict = false) const override { Q_UNUSED(strict); return true; } float preferrednessLevelWith(const KoColorSpace*) const override { return 0.0; } }; #endif // _Ko_BASIC_HISTOGRAM_PRODUCERS_ diff --git a/libs/pigment/Mainpage.dox b/libs/pigment/Mainpage.dox index 16e5783c25..79f7487c05 100644 --- a/libs/pigment/Mainpage.dox +++ b/libs/pigment/Mainpage.dox @@ -1,15 +1,15 @@ /** * \mainpage * Pigment is a Color Manipulation System with pluggable color spaces. Color spaces * can be based on the LCMS library, or using - * the OpenCTL implmenetation of the Color + * the OpenCTL implmentation of the Color * Transformation Langboth are optional. * * Pigment color spaces offers support for many common manipulations and for * pixel composition. Pigment works on arrays of bytes: the color spaces interpret * the array of bytes as a pixel according to the pixel format defined by each * color space. */ // DOXYGEN_SET_PROJECT_NAME = KritaPigment // DOXYGEN_SET_IGNORE_PREFIX = Ko K diff --git a/libs/pigment/compositeops/KoCompositeOpFunctions.h b/libs/pigment/compositeops/KoCompositeOpFunctions.h index d51ad77299..7357238e0e 100644 --- a/libs/pigment/compositeops/KoCompositeOpFunctions.h +++ b/libs/pigment/compositeops/KoCompositeOpFunctions.h @@ -1,952 +1,952 @@ /* * Copyright (c) 2011 Silvio Heinrich * * 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. */ #ifndef KOCOMPOSITEOP_FUNCTIONS_H_ #define KOCOMPOSITEOP_FUNCTIONS_H_ #include template inline void cfReorientedNormalMapCombine(TReal srcR, TReal srcG, TReal srcB, TReal& dstR, TReal& dstG, TReal& dstB) { // see http://blog.selfshadow.com/publications/blending-in-detail/ by Barre-Brisebois and Hill TReal tx = 2*srcR-1; TReal ty = 2*srcG-1; TReal tz = 2*srcB; TReal ux = -2*dstR+1; TReal uy = -2*dstG+1; TReal uz = 2*dstB-1; TReal k = (tx*ux+ty*uy+tz*uz)/tz; // dot(t,u)/t.z TReal rx = tx*k-ux; TReal ry = ty*k-uy; TReal rz = tz*k-uz; k = 1/sqrt(rx*rx+ry*ry+rz*rz); // normalize result rx *= k; ry *= k; rz *= k; dstR = rx*0.5+0.5; dstG = ry*0.5+0.5; dstB = rz*0.5+0.5; } template inline void cfColor(TReal sr, TReal sg, TReal sb, TReal& dr, TReal& dg, TReal& db) { TReal lum = getLightness(dr, dg, db); dr = sr; dg = sg; db = sb; setLightness(dr, dg, db, lum); } template inline void cfLightness(TReal sr, TReal sg, TReal sb, TReal& dr, TReal& dg, TReal& db) { setLightness(dr, dg, db, getLightness(sr, sg, sb)); } template inline void cfIncreaseLightness(TReal sr, TReal sg, TReal sb, TReal& dr, TReal& dg, TReal& db) { addLightness(dr, dg, db, getLightness(sr, sg, sb)); } template inline void cfDecreaseLightness(TReal sr, TReal sg, TReal sb, TReal& dr, TReal& dg, TReal& db) { addLightness(dr, dg, db, getLightness(sr, sg, sb) - TReal(1.0)); } template inline void cfSaturation(TReal sr, TReal sg, TReal sb, TReal& dr, TReal& dg, TReal& db) { TReal sat = getSaturation(sr, sg, sb); TReal light = getLightness(dr, dg, db); setSaturation(dr, dg, db, sat); setLightness(dr, dg, db, light); } template inline void cfIncreaseSaturation(TReal sr, TReal sg, TReal sb, TReal& dr, TReal& dg, TReal& db) { using namespace Arithmetic; TReal sat = lerp(getSaturation(dr,dg,db), unitValue(), getSaturation(sr,sg,sb)); TReal light = getLightness(dr, dg, db); setSaturation(dr, dg, db, sat); setLightness(dr, dg, db, light); } template inline void cfDecreaseSaturation(TReal sr, TReal sg, TReal sb, TReal& dr, TReal& dg, TReal& db) { using namespace Arithmetic; TReal sat = lerp(zeroValue(), getSaturation(dr,dg,db), getSaturation(sr,sg,sb)); TReal light = getLightness(dr, dg, db); setSaturation(dr, dg, db, sat); setLightness(dr, dg, db, light); } template inline void cfHue(TReal sr, TReal sg, TReal sb, TReal& dr, TReal& dg, TReal& db) { TReal sat = getSaturation(dr, dg, db); TReal lum = getLightness(dr, dg, db); dr = sr; dg = sg; db = sb; setSaturation(dr, dg, db, sat); setLightness(dr, dg, db, lum); } template inline void cfTangentNormalmap(TReal sr, TReal sg, TReal sb, TReal& dr, TReal& dg, TReal& db) { using namespace Arithmetic; TReal half=halfValue(); dr = sr+(dr-half); dg = sg+(dg-half); db = sb+(db-unitValue()); } template inline void cfDarkerColor(TReal sr, TReal sg, TReal sb, TReal& dr, TReal& dg, TReal& db) { TReal lum = getLightness(dr, dg, db); TReal lum2 = getLightness(sr, sg, sb); if (lum inline void cfLighterColor(TReal sr, TReal sg, TReal sb, TReal& dr, TReal& dg, TReal& db) { TReal lum = getLightness(dr, dg, db); TReal lum2 = getLightness(sr, sg, sb); if (lum>lum2) { sr = dr; sg = dg; sb = db; } else { dr = sr; dg = sg; db = sb; } } template inline T cfColorBurn(T src, T dst) { using namespace Arithmetic; if(dst == unitValue()) return unitValue(); T invDst = inv(dst); if(src < invDst) return zeroValue(); return inv(clamp(div(invDst, src))); } template inline T cfLinearBurn(T src, T dst) { using namespace Arithmetic; typedef typename KoColorSpaceMathsTraits::compositetype composite_type; return clamp(composite_type(src) + dst - unitValue()); } template inline T cfColorDodge(T src, T dst) { using namespace Arithmetic; //Fixing Color Dodge to avoid ZX Colors on bright area. if(src == unitValue()) return unitValue(); T invSrc = inv(src); if(invSrc == zeroValue()) return unitValue(); return Arithmetic::clamp(div(dst, invSrc)); } template inline T cfAddition(T src, T dst) { typedef typename KoColorSpaceMathsTraits::compositetype composite_type; return Arithmetic::clamp(composite_type(src) + dst); } template inline T cfSubtract(T src, T dst) { using namespace Arithmetic; typedef typename KoColorSpaceMathsTraits::compositetype composite_type; return clamp(composite_type(dst) - src); } template inline T cfInverseSubtract(T src, T dst) { using namespace Arithmetic; typedef typename KoColorSpaceMathsTraits::compositetype composite_type; return clamp(composite_type(dst) - inv(src)); } template inline T cfExclusion(T src, T dst) { using namespace Arithmetic; typedef typename KoColorSpaceMathsTraits::compositetype composite_type; composite_type x = mul(src, dst); return clamp(composite_type(dst) + src - (x + x)); } template inline T cfDivide(T src, T dst) { using namespace Arithmetic; //typedef typename KoColorSpaceMathsTraits::compositetype composite_type; if(src == zeroValue()) return (dst == zeroValue()) ? zeroValue() : unitValue(); return clamp(div(dst, src)); } template inline T cfHardLight(T src, T dst) { using namespace Arithmetic; typedef typename KoColorSpaceMathsTraits::compositetype composite_type; composite_type src2 = composite_type(src) + src; if(src > halfValue()) { // screen(src*2.0 - 1.0, dst) src2 -= unitValue(); // src2 is guaranteed to be smaller than unitValue() now return Arithmetic::unionShapeOpacity(T(src2), dst); } // src2 is guaranteed to be smaller than unitValue() due to 'if' return Arithmetic::mul(T(src2), dst); } template inline T cfSoftLightSvg(T src, T dst) { using namespace Arithmetic; qreal fsrc = scale(src); qreal fdst = scale(dst); if(fsrc > 0.5f) { qreal D = (fdst > 0.25f) ? sqrt(fdst) : ((16.0f*fdst - 12.0)*fdst + 4.0f)*fdst; return scale(fdst + (2.0f*fsrc - 1.0f) * (D - fdst)); } return scale(fdst - (1.0f - 2.0f * fsrc) * fdst * (1.0f - fdst)); } template inline T cfSoftLight(T src, T dst) { using namespace Arithmetic; qreal fsrc = scale(src); qreal fdst = scale(dst); if(fsrc > 0.5f) { return scale(fdst + (2.0f * fsrc - 1.0f) * (sqrt(fdst) - fdst)); } return scale(fdst - (1.0f - 2.0f*fsrc) * fdst * (1.0f - fdst)); } template inline T cfVividLight(T src, T dst) { using namespace Arithmetic; typedef typename KoColorSpaceMathsTraits::compositetype composite_type; if(src < halfValue()) { if(src == zeroValue()) return (dst == unitValue()) ? unitValue() : zeroValue(); // min(1,max(0,1-(1-dst) / (2*src))) composite_type src2 = composite_type(src) + src; composite_type dsti = inv(dst); return clamp(unitValue() - (dsti * unitValue() / src2)); } if(src == unitValue()) return (dst == zeroValue()) ? zeroValue() : unitValue(); // min(1,max(0, dst / (2*(1-src))) composite_type srci2 = inv(src); srci2 += srci2; return clamp(composite_type(dst) * unitValue() / srci2); } template inline T cfPinLight(T src, T dst) { typedef typename KoColorSpaceMathsTraits::compositetype composite_type; // TODO: verify that the formula is correct (the first max would be useless here) // max(0, max(2*src-1, min(dst, 2*src))) composite_type src2 = composite_type(src) + src; composite_type a = qMin(dst, src2); composite_type b = qMax(src2-Arithmetic::unitValue(), a); return T(b); } template inline T cfArcTangent(T src, T dst) { using namespace Arithmetic; if(dst == zeroValue()) return (src == zeroValue()) ? zeroValue() : unitValue(); return scale(2.0 * atan(scale(src) / scale(dst)) / Arithmetic::pi); } template inline T cfAllanon(T src, T dst) { using namespace Arithmetic; typedef typename KoColorSpaceMathsTraits::compositetype composite_type; // (dst + src) / 2 [or (dst + src) * 0.5] return T((composite_type(src) + dst) * halfValue() / unitValue()); } template inline T cfLinearLight(T src, T dst) { using namespace Arithmetic; typedef typename KoColorSpaceMathsTraits::compositetype composite_type; // min(1,max(0,(dst + 2*src)-1)) return clamp((composite_type(src) + src + dst) - unitValue()); } template inline T cfParallel(T src, T dst) { using namespace Arithmetic; typedef typename KoColorSpaceMathsTraits::compositetype composite_type; // min(max(2 / (1/dst + 1/src), 0), 1) composite_type unit = unitValue(); composite_type s = (src != zeroValue()) ? div(unit, src) : unit; composite_type d = (dst != zeroValue()) ? div(unit, dst) : unit; if (src == zeroValue()) { return zeroValue(); } if (dst == zeroValue()) { return zeroValue(); } return clamp((unit+unit) * unit / (d+s)); } template inline T cfEquivalence(T src, T dst) { typedef typename KoColorSpaceMathsTraits::compositetype composite_type; // 1 - abs(dst - src) composite_type x = composite_type(dst) - src; return (x < Arithmetic::zeroValue()) ? T(-x) : T(x); } template inline T cfGrainMerge(T src, T dst) { using namespace Arithmetic; typedef typename KoColorSpaceMathsTraits::compositetype composite_type; return clamp(composite_type(dst) + src - halfValue()); } template inline T cfGrainExtract(T src, T dst) { using namespace Arithmetic; typedef typename KoColorSpaceMathsTraits::compositetype composite_type; return clamp(composite_type(dst) - src + halfValue()); } template inline T cfHardMix(T src, T dst) { return (dst > Arithmetic::halfValue()) ? cfColorDodge(src,dst) : cfColorBurn(src,dst); } template inline T cfHardMixPhotoshop(T src, T dst) { using namespace Arithmetic; typedef typename KoColorSpaceMathsTraits::compositetype composite_type; const composite_type sum = composite_type(src) + dst; return sum > unitValue() ? unitValue() : zeroValue(); } template inline T cfAdditiveSubtractive(T src, T dst) { using namespace Arithmetic; // min(1,max(0,abs(sqr(CB)-sqr(CT)))) qreal x = sqrt(scale(dst)) - sqrt(scale(src)); return scale((x < 0.0) ? -x : x); } template inline T cfGammaDark(T src, T dst) { using namespace Arithmetic; if(src == zeroValue()) return zeroValue(); // power(dst, 1/src) return scale(pow(scale(dst), 1.0/scale(src))); } template inline T cfGammaLight(T src, T dst) { using namespace Arithmetic; return scale(pow(scale(dst), scale(src))); } template inline T cfGammaIllumination(T src, T dst) { using namespace Arithmetic; return inv(cfGammaDark(inv(src),inv(dst))); } template inline T cfGeometricMean(T src, T dst) { using namespace Arithmetic; return scale(sqrt(scale(dst) * scale(src))); } template inline T cfOver(T src, T dst) { Q_UNUSED(dst); return src; } template inline T cfOverlay(T src, T dst) { return cfHardLight(dst, src); } template inline T cfMultiply(T src, T dst) { return Arithmetic::mul(src, dst); } template inline T cfHardOverlay(T src, T dst) { using namespace Arithmetic; qreal fsrc = scale(src); qreal fdst = scale(dst); if (fsrc == 1.0) { return scale(1.0);} if(fsrc > 0.5f) { return scale(cfDivide(inv(2.0 * fsrc - 1.0f), fdst)); } return scale(mul(2.0 * fsrc, fdst)); } template inline T cfDifference(T src, T dst) { return qMax(src,dst) - qMin(src,dst); } template inline T cfScreen(T src, T dst) { return Arithmetic::unionShapeOpacity(src, dst); } template inline T cfDarkenOnly(T src, T dst) { return qMin(src, dst); } template inline T cfLightenOnly(T src, T dst) { return qMax(src, dst); } template inline T cfGlow(T src, T dst) { using namespace Arithmetic; // see http://www.pegtop.net/delphi/articles/blendmodes/quadratic.htm for formulas of Quadratic Blending Modes like Glow, Reflect, Freeze, and Heat if (dst == unitValue()) { return unitValue(); } return clamp(div(mul(src, src), inv(dst))); } template inline T cfReflect(T src, T dst) { using namespace Arithmetic; return clamp(cfGlow(dst,src)); } template inline T cfHeat(T src, T dst) { using namespace Arithmetic; if(src == unitValue()) { return unitValue(); } if(dst == zeroValue()) { return zeroValue(); } return inv(clamp(div(mul(inv(src), inv(src)),dst))); } template inline T cfFreeze(T src, T dst) { using namespace Arithmetic; return (cfHeat(dst,src)); } template inline T cfHelow(T src, T dst) { using namespace Arithmetic; // see http://www.pegtop.net/delphi/articles/blendmodes/quadratic.htm for formulas of Quadratic Blending Modes like Glow, Reflect, Freeze, and Heat if (cfHardMixPhotoshop(src,dst) == unitValue()) { return cfHeat(src,dst); } if (src == zeroValue()) { return zeroValue(); } return (cfGlow(src,dst)); } template inline T cfFrect(T src, T dst) { using namespace Arithmetic; if (cfHardMixPhotoshop(src,dst) == unitValue()) { return cfFreeze(src,dst); } if (dst == zeroValue()) { return zeroValue(); } return (cfReflect(src,dst)); } template inline T cfGleat(T src, T dst) { using namespace Arithmetic; // see http://www.pegtop.net/delphi/articles/blendmodes/quadratic.htm for formulas of Quadratic Blending Modes like Glow, Reflect, Freeze, and Heat if(dst == unitValue()) { return unitValue(); } if(cfHardMixPhotoshop(src,dst) == unitValue()) { return cfGlow(src,dst); } return (cfHeat(src,dst)); } template inline T cfReeze(T src, T dst) { using namespace Arithmetic; return (cfGleat(dst,src)); } template inline T cfFhyrd(T src, T dst) { using namespace Arithmetic; return (cfAllanon(cfFrect(src,dst),cfHelow(src,dst))); } template inline T cfInterpolation(T src, T dst) { using namespace Arithmetic; qreal fsrc = scale(src); qreal fdst = scale(dst); if(dst == zeroValue() && src == zeroValue()) { return zeroValue(); } return scale(.5f-.25f*cos(pi*(fsrc))-.25f*cos(pi*(fdst))); } template inline T cfInterpolationB(T src, T dst) { using namespace Arithmetic; return cfInterpolation(cfInterpolation(src,dst),cfInterpolation(src,dst)); } template inline T cfPenumbraB(T src, T dst) { using namespace Arithmetic; if (dst == unitValue()) { return unitValue(); } if (dst + src < unitValue()) { return (cfColorDodge(dst,src)/2); } if (src == zeroValue()) { return zeroValue(); } return inv(clamp(div(inv(dst),src)/2)); } template inline T cfPenumbraD(T src, T dst) { using namespace Arithmetic; if (dst == unitValue()) { return unitValue(); } return cfArcTangent(src,inv(dst)); } template inline T cfPenumbraC(T src, T dst) { using namespace Arithmetic; return cfPenumbraD(dst,src); } template inline T cfPenumbraA(T src, T dst) { using namespace Arithmetic; return (cfPenumbraB(dst,src)); } template inline T cfSoftLightIFSIllusions(T src, T dst) { using namespace Arithmetic; qreal fsrc = scale(src); qreal fdst = scale(dst); return scale(pow(fdst,pow(2.0,(mul(2.0,.5f-fsrc))))); } template inline T cfSoftLightPegtopDelphi(T src, T dst) { using namespace Arithmetic; return clamp(cfAddition(mul(dst,cfScreen(src,dst)),mul(mul(src,dst),inv(dst)))); } template inline T cfNegation(T src, T dst) { using namespace Arithmetic; typedef typename KoColorSpaceMathsTraits::compositetype composite_type; composite_type unit = unitValue(); composite_type a = unit - src - dst; composite_type s = abs(a); composite_type d = unit - s; return T(d); } template inline T cfNor(T src, T dst) { using namespace Arithmetic; return and(src,dst); } template inline T cfNand(T src, T dst) { using namespace Arithmetic; return or(src,dst); } template inline T cfXor(T src, T dst) { using namespace Arithmetic; return xor(src,dst); } template inline T cfXnor(T src, T dst) { using namespace Arithmetic; return cfXor(src,inv(dst)); } template inline T cfAnd(T src, T dst) { using namespace Arithmetic; return cfNor(inv(src),inv(dst)); } template inline T cfOr(T src, T dst) { using namespace Arithmetic; return cfNand(inv(src),inv(dst)); } template inline T cfConverse(T src, T dst) { using namespace Arithmetic; return cfOr(inv(src),dst); } template inline T cfNotConverse(T src, T dst) { using namespace Arithmetic; return cfAnd(src,inv(dst)); } template inline T cfImplies(T src, T dst) { using namespace Arithmetic; return cfOr(src,inv(dst)); } template inline T cfNotImplies(T src, T dst) { using namespace Arithmetic; return cfAnd(inv(src),dst); } template inline T cfPNormA(T src, T dst) { using namespace Arithmetic; //This is also known as P-Norm mode with factor of 2.3333 See IMBLEND image blending mode samples, and please see imblend.m file found on Additional Blending Mode thread at Phabricator. 1/2.3333 is .42875... return clamp(pow(pow((float)dst, 2.3333333333333333) + pow((float)src, 2.3333333333333333), 0.428571428571434)); } template inline T cfPNormB(T src, T dst) { using namespace Arithmetic; //This is also known as P-Norm mode with factor of 2.3333 See IMBLEND image blending mode samples, and please see imblend.m file found on Additional Blending Mode thread at Phabricator. 1/2.3333 is .42875... return clamp(pow(pow(dst,4)+pow(src,4),0.25)); } template inline T cfSuperLight(T src, T dst) { using namespace Arithmetic; //4.0 can be adjusted to taste. 4.0 is picked for being the best in terms of contrast and details. See imblend.m file. qreal fsrc = scale(src); qreal fdst = scale(dst); if (fsrc < .5) { return scale(inv(pow(pow(inv(fdst),2.875)+pow(inv(2.0*fsrc),2.875),1.0/2.875))); } return scale(pow(pow(fdst,2.875)+pow(2.0*fsrc-1.0,2.875),1.0/2.875)); } template inline T cfTintIFSIllusions(T src, T dst) { using namespace Arithmetic; //Known as Light Blending mode found in IFS Illusions. Picked this name because it results into a very strong tint, and has better naming convention. qreal fsrc = scale(src); qreal fdst = scale(dst); return scale(fsrc*inv(fdst)+sqrt(fdst)); } template inline T cfShadeIFSIllusions(T src, T dst) { using namespace Arithmetic; //Known as Shadow Blending mode found in IFS Illusions. Picked this name because it is the opposite of Tint (IFS Illusion Blending mode). qreal fsrc = scale(src); qreal fdst = scale(dst); return scale(inv((inv(fdst)*fsrc)+sqrt(inv(fsrc)))); } template inline T cfFogLightenIFSIllusions(T src, T dst) { using namespace Arithmetic; - //Known as Bright Blending mode found in IFS Illusions. Picked this name because the shading reminds me of fog when overlaying with a gradientt. + //Known as Bright Blending mode found in IFS Illusions. Picked this name because the shading reminds me of fog when overlaying with a gradient. qreal fsrc = scale(src); qreal fdst = scale(dst); if (fsrc < .5) { return scale(inv(inv(fsrc)*fsrc)-inv(fdst)*inv(fsrc)); } return scale(fsrc-inv(fdst)*inv(fsrc)+pow(inv(fsrc),2)); } template inline T cfFogDarkenIFSIllusions(T src, T dst) { using namespace Arithmetic; //Known as Dark Blending mode found in IFS Illusions. Picked this name because the shading reminds me of fog when overlaying with a gradient. qreal fsrc = scale(src); qreal fdst = scale(dst); if (fsrc < .5) { return scale(inv(fsrc)*fsrc+fsrc*fdst); } return scale(fsrc*fdst+fsrc-pow(fsrc,2)); } template inline T cfModulo(T src, T dst) { using namespace Arithmetic; return mod(dst,src); } template inline T cfModuloShift(T src, T dst) { using namespace Arithmetic; qreal fsrc = scale(src); qreal fdst = scale(dst); if (fsrc == 1.0 && fdst == 0.0) { return scale(0.0); } return scale(mod((fdst+fsrc),1.0000000000)); } template inline T cfModuloShiftContinuous(T src, T dst) { using namespace Arithmetic; - //This blending mode do not behave like difference/equilavent with destination layer inverted if you use group layer on addition while the content of group layer contains several addition-mode layers, it works as expected on float images. So, no need to change this. + //This blending mode do not behave like difference/equivalent with destination layer inverted if you use group layer on addition while the content of group layer contains several addition-mode layers, it works as expected on float images. So, no need to change this. qreal fsrc = scale(src); qreal fdst = scale(dst); if (fsrc == 1.0 && fdst == 0.0) { return scale(1.0); } return scale((int(ceil(fdst+fsrc)) % 2 != 0) || (fdst == zeroValue()) ? cfModuloShift(fsrc,fdst) : inv(cfModuloShift(fsrc,fdst))); } template inline T cfDivisiveModulo(T src, T dst) { using namespace Arithmetic; //I have to use 1.00000 as unitValue failed to work for those area. qreal fsrc = scale(src); qreal fdst = scale(dst); if (fsrc == zeroValue()) { return scale(mod(((1.0000000000/epsilon()) * fdst),1.0000000000)); } return scale(mod(((1.0000000000/fsrc) * fdst),1.0000000000)); } template inline T cfDivisiveModuloContinuous(T src, T dst) { using namespace Arithmetic; qreal fsrc = scale(src); qreal fdst = scale(dst); if (fdst == zeroValue()) { return zeroValue(); } if (fsrc == zeroValue()) { return cfDivisiveModulo(fsrc,fdst); } return scale( int(ceil(fdst/fsrc)) % 2 != 0 ? cfDivisiveModulo(fsrc,fdst) : inv(cfDivisiveModulo(fsrc,fdst))); } template inline T cfModuloContinuous(T src, T dst) { using namespace Arithmetic; return cfMultiply(cfDivisiveModuloContinuous(src,dst),src); } template inline T cfEasyDodge(T src, T dst) { using namespace Arithmetic; // The 13 divided by 15 can be adjusted to taste. See imgblend.m qreal fsrc = scale(src); qreal fdst = scale(dst); if (fsrc == 1.0) { return scale(1.0);} return scale(pow(fdst,mul(inv(fsrc != 1.0 ? fsrc : .999999999999),1.039999999))); } template inline T cfEasyBurn(T src, T dst) { using namespace Arithmetic; // The 13 divided by 15 can be adjusted to taste. See imgblend.m qreal fsrc = scale(src); qreal fdst = scale(dst); return scale(inv(pow(inv(fsrc != 1.0 ? fsrc : .999999999999),mul(fdst,1.039999999)))); } template inline T cfFlatLight(T src, T dst) { using namespace Arithmetic; if (src == zeroValue()) { return zeroValue(); } return clamp(cfHardMixPhotoshop(inv(src),dst)==unitValue() ? cfPenumbraB(src,dst) : cfPenumbraA(src,dst)); } template inline void cfAdditionSAI(TReal src, TReal sa, TReal& dst, TReal& da) { using namespace Arithmetic; typedef typename KoColorSpaceMathsTraits::compositetype composite_type; Q_UNUSED(da); composite_type newsrc; newsrc = mul(src, sa); dst = clamp(newsrc + dst); } #endif // KOCOMPOSITEOP_FUNCTIONS_H_ diff --git a/libs/pigment/compositeops/KoCompositeOps.h b/libs/pigment/compositeops/KoCompositeOps.h index b29c4d287e..b264284884 100644 --- a/libs/pigment/compositeops/KoCompositeOps.h +++ b/libs/pigment/compositeops/KoCompositeOps.h @@ -1,364 +1,364 @@ /* * Copyright (c) 2007 Cyrille Berger * Copyright (c) 2011 Silvio Heinrich * * 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. */ #ifndef _KOCOMPOSITEOPS_H_ #define _KOCOMPOSITEOPS_H_ #include #include #include #include #include "compositeops/KoCompositeOpGeneric.h" #include "compositeops/KoCompositeOpOver.h" #include "compositeops/KoCompositeOpCopyChannel.h" #include "compositeops/KoCompositeOpAlphaDarken.h" #include "compositeops/KoCompositeOpErase.h" #include "compositeops/KoCompositeOpCopy2.h" #include "compositeops/KoCompositeOpDissolve.h" #include "compositeops/KoCompositeOpBehind.h" #include "compositeops/KoCompositeOpDestinationIn.h" #include "compositeops/KoCompositeOpDestinationAtop.h" #include "compositeops/KoCompositeOpGreater.h" #include "compositeops/KoAlphaDarkenParamsWrapper.h" #include "KoOptimizedCompositeOpFactory.h" namespace _Private { template struct AddGeneralOps { static void add(KoColorSpace* cs) { Q_UNUSED(cs); } }; template struct OptimizedOpsSelector { static KoCompositeOp* createAlphaDarkenOp(const KoColorSpace *cs) { if (useCreamyAlphaDarken()) { return new KoCompositeOpAlphaDarken(cs); } else { return new KoCompositeOpAlphaDarken(cs); } } static KoCompositeOp* createOverOp(const KoColorSpace *cs) { return new KoCompositeOpOver(cs); } }; template<> struct OptimizedOpsSelector { static KoCompositeOp* createAlphaDarkenOp(const KoColorSpace *cs) { return useCreamyAlphaDarken() ? KoOptimizedCompositeOpFactory::createAlphaDarkenOpCreamy32(cs) : KoOptimizedCompositeOpFactory::createAlphaDarkenOpHard32(cs); } static KoCompositeOp* createOverOp(const KoColorSpace *cs) { return KoOptimizedCompositeOpFactory::createOverOp32(cs); } }; template<> struct OptimizedOpsSelector { static KoCompositeOp* createAlphaDarkenOp(const KoColorSpace *cs) { return useCreamyAlphaDarken() ? KoOptimizedCompositeOpFactory::createAlphaDarkenOpCreamy32(cs) : KoOptimizedCompositeOpFactory::createAlphaDarkenOpHard32(cs); } static KoCompositeOp* createOverOp(const KoColorSpace *cs) { return KoOptimizedCompositeOpFactory::createOverOp32(cs); } }; template<> struct OptimizedOpsSelector { static KoCompositeOp* createAlphaDarkenOp(const KoColorSpace *cs) { // TODO: optimized code is disabled for 4.2 release, - // becasue it causes bug https://bugs.kde.org/show_bug.cgi?id=404133 + // because it causes bug https://bugs.kde.org/show_bug.cgi?id=404133 if (useCreamyAlphaDarken()) { return new KoCompositeOpAlphaDarken(cs); } else { return new KoCompositeOpAlphaDarken(cs); } // TODO: please restore this optimized version when the bug is fixed // return useCreamyAlphaDarken() ? // KoOptimizedCompositeOpFactory::createAlphaDarkenOpCreamy128(cs) : // KoOptimizedCompositeOpFactory::createAlphaDarkenOpHard128(cs); } static KoCompositeOp* createOverOp(const KoColorSpace *cs) { return KoOptimizedCompositeOpFactory::createOverOp128(cs); } }; template struct AddGeneralOps { typedef typename Traits::channels_type Arg; typedef Arg (*CompositeFunc)(Arg, Arg); static const qint32 alpha_pos = Traits::alpha_pos; template static void add(KoColorSpace* cs, const QString& id, const QString& description, const QString& category) { cs->addCompositeOp(new KoCompositeOpGenericSC(cs, id, description, category)); } static void add(KoColorSpace* cs) { cs->addCompositeOp(OptimizedOpsSelector::createOverOp(cs)); cs->addCompositeOp(OptimizedOpsSelector::createAlphaDarkenOp(cs)); cs->addCompositeOp(new KoCompositeOpCopy2(cs)); cs->addCompositeOp(new KoCompositeOpErase(cs)); cs->addCompositeOp(new KoCompositeOpBehind(cs)); cs->addCompositeOp(new KoCompositeOpDestinationIn(cs)); cs->addCompositeOp(new KoCompositeOpDestinationAtop(cs)); cs->addCompositeOp(new KoCompositeOpGreater(cs)); add<&cfOverlay >(cs, COMPOSITE_OVERLAY , i18n("Overlay") , KoCompositeOp::categoryMix()); add<&cfGrainMerge >(cs, COMPOSITE_GRAIN_MERGE , i18n("Grain Merge") , KoCompositeOp::categoryMix()); add<&cfGrainExtract >(cs, COMPOSITE_GRAIN_EXTRACT , i18n("Grain Extract") , KoCompositeOp::categoryMix()); add<&cfHardMix >(cs, COMPOSITE_HARD_MIX , i18n("Hard Mix") , KoCompositeOp::categoryMix()); add<&cfHardMixPhotoshop>(cs, COMPOSITE_HARD_MIX_PHOTOSHOP, i18n("Hard Mix (Photoshop)") , KoCompositeOp::categoryMix()); add<&cfGeometricMean >(cs, COMPOSITE_GEOMETRIC_MEAN, i18n("Geometric Mean"), KoCompositeOp::categoryMix()); add<&cfParallel >(cs, COMPOSITE_PARALLEL , i18n("Parallel") , KoCompositeOp::categoryMix()); add<&cfAllanon >(cs, COMPOSITE_ALLANON , i18n("Allanon") , KoCompositeOp::categoryMix()); add<&cfHardOverlay >(cs, COMPOSITE_HARD_OVERLAY , i18n("Hard Overlay") , KoCompositeOp::categoryMix()); add<&cfInterpolation >(cs, COMPOSITE_INTERPOLATION , i18n("Interpolation") , KoCompositeOp::categoryMix()); add<&cfInterpolationB >(cs, COMPOSITE_INTERPOLATIONB , i18n("Interpolation - 2X") , KoCompositeOp::categoryMix()); add<&cfPenumbraA >(cs, COMPOSITE_PENUMBRAA , i18n("Penumbra A") , KoCompositeOp::categoryMix()); add<&cfPenumbraB >(cs, COMPOSITE_PENUMBRAB , i18n("Penumbra B") , KoCompositeOp::categoryMix()); add<&cfPenumbraC >(cs, COMPOSITE_PENUMBRAC , i18n("Penumbra C") , KoCompositeOp::categoryMix()); add<&cfPenumbraD >(cs, COMPOSITE_PENUMBRAD , i18n("Penumbra D") , KoCompositeOp::categoryMix()); add<&cfScreen >(cs, COMPOSITE_SCREEN , i18n("Screen") , KoCompositeOp::categoryLight()); add<&cfColorDodge >(cs, COMPOSITE_DODGE , i18n("Color Dodge") , KoCompositeOp::categoryLight()); add<&cfAddition >(cs, COMPOSITE_LINEAR_DODGE, i18n("Linear Dodge"), KoCompositeOp::categoryLight()); add<&cfLightenOnly >(cs, COMPOSITE_LIGHTEN , i18n("Lighten") , KoCompositeOp::categoryLight()); add<&cfHardLight >(cs, COMPOSITE_HARD_LIGHT , i18n("Hard Light") , KoCompositeOp::categoryLight()); add<&cfSoftLightIFSIllusions>(cs, COMPOSITE_SOFT_LIGHT_IFS_ILLUSIONS, i18n("Soft Light (IFS Illusions)") , KoCompositeOp::categoryLight()); add<&cfSoftLightPegtopDelphi>(cs, COMPOSITE_SOFT_LIGHT_PEGTOP_DELPHI, i18n("Soft Light (Pegtop-Delphi)") , KoCompositeOp::categoryLight()); add<&cfSoftLightSvg >(cs, COMPOSITE_SOFT_LIGHT_SVG, i18n("Soft Light (SVG)") , KoCompositeOp::categoryLight()); add<&cfSoftLight >(cs, COMPOSITE_SOFT_LIGHT_PHOTOSHOP, i18n("Soft Light (Photoshop)") , KoCompositeOp::categoryLight()); add<&cfGammaLight >(cs, COMPOSITE_GAMMA_LIGHT , i18n("Gamma Light") , KoCompositeOp::categoryLight()); add<&cfGammaIllumination >(cs, COMPOSITE_GAMMA_ILLUMINATION , i18n("Gamma Illumination") , KoCompositeOp::categoryLight()); add<&cfVividLight >(cs, COMPOSITE_VIVID_LIGHT , i18n("Vivid Light") , KoCompositeOp::categoryLight()); add<&cfFlatLight >(cs, COMPOSITE_FLAT_LIGHT , i18n("Flat Light") , KoCompositeOp::categoryLight()); add<&cfPinLight >(cs, COMPOSITE_PIN_LIGHT , i18n("Pin Light") , KoCompositeOp::categoryLight()); add<&cfLinearLight >(cs, COMPOSITE_LINEAR_LIGHT, i18n("Linear Light"), KoCompositeOp::categoryLight()); add<&cfPNormA >(cs, COMPOSITE_PNORM_A , i18n("P-Norm A") , KoCompositeOp::categoryLight()); add<&cfPNormB >(cs, COMPOSITE_PNORM_B , i18n("P-Norm B") , KoCompositeOp::categoryLight()); add<&cfSuperLight >(cs, COMPOSITE_SUPER_LIGHT , i18n("Super Light") , KoCompositeOp::categoryLight()); add<&cfTintIFSIllusions >(cs, COMPOSITE_TINT_IFS_ILLUSIONS , i18n("Tint (IFS Illusions)") , KoCompositeOp::categoryLight()); add<&cfFogLightenIFSIllusions >(cs, COMPOSITE_FOG_LIGHTEN_IFS_ILLUSIONS , i18n("Fog Lighten (IFS Illusions)") , KoCompositeOp::categoryLight()); add<&cfEasyDodge >(cs, COMPOSITE_EASY_DODGE , i18n("Easy Dodge") , KoCompositeOp::categoryLight()); add<&cfColorBurn >(cs, COMPOSITE_BURN , i18n("Color Burn") , KoCompositeOp::categoryDark()); add<&cfLinearBurn >(cs, COMPOSITE_LINEAR_BURN , i18n("Linear Burn"), KoCompositeOp::categoryDark()); add<&cfDarkenOnly >(cs, COMPOSITE_DARKEN , i18n("Darken") , KoCompositeOp::categoryDark()); add<&cfGammaDark >(cs, COMPOSITE_GAMMA_DARK , i18n("Gamma Dark") , KoCompositeOp::categoryDark()); add<&cfShadeIFSIllusions >(cs, COMPOSITE_SHADE_IFS_ILLUSIONS , i18n("Shade (IFS_Illusions)") , KoCompositeOp::categoryDark()); add<&cfFogDarkenIFSIllusions >(cs, COMPOSITE_FOG_DARKEN_IFS_ILLUSIONS , i18n("Fog Darken (IFS Illusions)") , KoCompositeOp::categoryDark()); add<&cfEasyBurn >(cs, COMPOSITE_EASY_BURN , i18n("Easy Burn") , KoCompositeOp::categoryDark()); add<&cfAddition >(cs, COMPOSITE_ADD , i18n("Addition") , KoCompositeOp::categoryArithmetic()); add<&cfSubtract >(cs, COMPOSITE_SUBTRACT , i18n("Subtract") , KoCompositeOp::categoryArithmetic()); add<&cfInverseSubtract >(cs, COMPOSITE_INVERSE_SUBTRACT, i18n("Inversed-Subtract"), KoCompositeOp::categoryArithmetic()); add<&cfMultiply >(cs, COMPOSITE_MULT , i18n("Multiply") , KoCompositeOp::categoryArithmetic()); add<&cfDivide >(cs, COMPOSITE_DIVIDE , i18n("Divide") , KoCompositeOp::categoryArithmetic()); add<&cfModulo >(cs, COMPOSITE_MOD , i18n("Modulo") , KoCompositeOp::categoryModulo()); add<&cfModuloContinuous >(cs, COMPOSITE_MOD_CON , i18n("Modulo - Continuous") , KoCompositeOp::categoryModulo()); add<&cfDivisiveModulo >(cs, COMPOSITE_DIVISIVE_MOD , i18n("Divisive Modulo") , KoCompositeOp::categoryModulo()); add<&cfDivisiveModuloContinuous >(cs, COMPOSITE_DIVISIVE_MOD_CON , i18n("Divisive Modulo - Continuous") , KoCompositeOp::categoryModulo()); add<&cfModuloShift >(cs, COMPOSITE_MODULO_SHIFT , i18n("Modulo Shift") , KoCompositeOp::categoryModulo()); add<&cfModuloShiftContinuous >(cs, COMPOSITE_MODULO_SHIFT_CON , i18n("Modulo Shift - Continuous") , KoCompositeOp::categoryModulo()); add<&cfArcTangent >(cs, COMPOSITE_ARC_TANGENT , i18n("Arcus Tangent") , KoCompositeOp::categoryNegative()); add<&cfDifference >(cs, COMPOSITE_DIFF , i18n("Difference") , KoCompositeOp::categoryNegative()); add<&cfExclusion >(cs, COMPOSITE_EXCLUSION , i18n("Exclusion") , KoCompositeOp::categoryNegative()); add<&cfEquivalence >(cs, COMPOSITE_EQUIVALENCE , i18n("Equivalence") , KoCompositeOp::categoryNegative()); add<&cfAdditiveSubtractive >(cs, COMPOSITE_ADDITIVE_SUBTRACTIVE , i18n("Additive-Subtractive") , KoCompositeOp::categoryNegative()); add<&cfNegation >(cs, COMPOSITE_NEGATION , i18n("Negation") , KoCompositeOp::categoryNegative()); add<&cfXor >(cs, COMPOSITE_XOR , i18n("XOR") , KoCompositeOp::categoryBinary()); add<&cfOr >(cs, COMPOSITE_OR , i18n("OR") , KoCompositeOp::categoryBinary()); add<&cfAnd >(cs, COMPOSITE_AND , i18n("AND") , KoCompositeOp::categoryBinary()); add<&cfNand >(cs, COMPOSITE_NAND , i18n("NAND") , KoCompositeOp::categoryBinary()); add<&cfNor >(cs, COMPOSITE_NOR , i18n("NOR") , KoCompositeOp::categoryBinary()); add<&cfXnor >(cs, COMPOSITE_XNOR , i18n("XNOR") , KoCompositeOp::categoryBinary()); add<&cfImplies >(cs, COMPOSITE_IMPLICATION , i18n("IMPLICATION") , KoCompositeOp::categoryBinary()); add<&cfNotImplies >(cs, COMPOSITE_NOT_IMPLICATION , i18n("NOT IMPLICATION") , KoCompositeOp::categoryBinary()); add<&cfConverse >(cs, COMPOSITE_CONVERSE , i18n("CONVERSE") , KoCompositeOp::categoryBinary()); add<&cfNotConverse >(cs, COMPOSITE_NOT_CONVERSE , i18n("NOT CONVERSE") , KoCompositeOp::categoryBinary()); add<&cfReflect >(cs, COMPOSITE_REFLECT , i18n("Reflect") , KoCompositeOp::categoryQuadratic()); add<&cfGlow >(cs, COMPOSITE_GLOW , i18n("Glow") , KoCompositeOp::categoryQuadratic()); add<&cfFreeze >(cs, COMPOSITE_FREEZE , i18n("Freeze") , KoCompositeOp::categoryQuadratic()); add<&cfHeat >(cs, COMPOSITE_HEAT , i18n("Heat") , KoCompositeOp::categoryQuadratic()); add<&cfGleat >(cs, COMPOSITE_GLEAT , i18n("Glow-Heat") , KoCompositeOp::categoryQuadratic()); add<&cfHelow >(cs, COMPOSITE_HELOW , i18n("Heat-Glow") , KoCompositeOp::categoryQuadratic()); add<&cfReeze >(cs, COMPOSITE_REEZE , i18n("Reflect-Freeze") , KoCompositeOp::categoryQuadratic()); add<&cfFrect >(cs, COMPOSITE_FRECT , i18n("Freeze-Reflect") , KoCompositeOp::categoryQuadratic()); add<&cfFhyrd >(cs, COMPOSITE_FHYRD , i18n("Heat-Glow & Freeze-Reflect Hybrid") , KoCompositeOp::categoryQuadratic()); cs->addCompositeOp(new KoCompositeOpDissolve(cs, KoCompositeOp::categoryMisc())); } }; template struct AddRGBOps { static void add(KoColorSpace* cs) { Q_UNUSED(cs); } }; template struct AddRGBOps { typedef float Arg; static const qint32 red_pos = Traits::red_pos; static const qint32 green_pos = Traits::green_pos; static const qint32 blue_pos = Traits::blue_pos; template static void add(KoColorSpace* cs, const QString& id, const QString& description, const QString& category) { cs->addCompositeOp(new KoCompositeOpGenericHSL(cs, id, description, category)); } static void add(KoColorSpace* cs) { cs->addCompositeOp(new KoCompositeOpCopyChannel(cs, COMPOSITE_COPY_RED , i18n("Copy Red") , KoCompositeOp::categoryMisc())); cs->addCompositeOp(new KoCompositeOpCopyChannel(cs, COMPOSITE_COPY_GREEN, i18n("Copy Green"), KoCompositeOp::categoryMisc())); cs->addCompositeOp(new KoCompositeOpCopyChannel(cs, COMPOSITE_COPY_BLUE , i18n("Copy Blue") , KoCompositeOp::categoryMisc())); add<&cfTangentNormalmap >(cs, COMPOSITE_TANGENT_NORMALMAP , i18n("Tangent Normalmap") , KoCompositeOp::categoryMisc()); add<&cfReorientedNormalMapCombine >(cs, COMPOSITE_COMBINE_NORMAL, i18n("Combine Normal Maps"), KoCompositeOp::categoryMisc()); add<&cfColor >(cs, COMPOSITE_COLOR , i18n("Color") , KoCompositeOp::categoryHSY()); add<&cfHue >(cs, COMPOSITE_HUE , i18n("Hue") , KoCompositeOp::categoryHSY()); add<&cfSaturation >(cs, COMPOSITE_SATURATION , i18n("Saturation") , KoCompositeOp::categoryHSY()); add<&cfIncreaseSaturation >(cs, COMPOSITE_INC_SATURATION, i18n("Increase Saturation"), KoCompositeOp::categoryHSY()); add<&cfDecreaseSaturation >(cs, COMPOSITE_DEC_SATURATION, i18n("Decrease Saturation"), KoCompositeOp::categoryHSY()); add<&cfLightness >(cs, COMPOSITE_LUMINIZE , i18n("Luminosity") , KoCompositeOp::categoryHSY()); add<&cfIncreaseLightness >(cs, COMPOSITE_INC_LUMINOSITY, i18n("Increase Luminosity"), KoCompositeOp::categoryHSY()); add<&cfDecreaseLightness >(cs, COMPOSITE_DEC_LUMINOSITY, i18n("Decrease Luminosity"), KoCompositeOp::categoryHSY()); add<&cfDarkerColor >(cs, COMPOSITE_DARKER_COLOR, i18n("Darker Color"), KoCompositeOp::categoryDark());//darker color as PSD does it// add<&cfLighterColor >(cs, COMPOSITE_LIGHTER_COLOR, i18n("Lighter Color"), KoCompositeOp::categoryLight());//lighter color as PSD does it// add<&cfColor >(cs, COMPOSITE_COLOR_HSI , i18n("Color HSI") , KoCompositeOp::categoryHSI()); add<&cfHue >(cs, COMPOSITE_HUE_HSI , i18n("Hue HSI") , KoCompositeOp::categoryHSI()); add<&cfSaturation >(cs, COMPOSITE_SATURATION_HSI , i18n("Saturation HSI") , KoCompositeOp::categoryHSI()); add<&cfIncreaseSaturation >(cs, COMPOSITE_INC_SATURATION_HSI, i18n("Increase Saturation HSI"), KoCompositeOp::categoryHSI()); add<&cfDecreaseSaturation >(cs, COMPOSITE_DEC_SATURATION_HSI, i18n("Decrease Saturation HSI"), KoCompositeOp::categoryHSI()); add<&cfLightness >(cs, COMPOSITE_INTENSITY , i18n("Intensity") , KoCompositeOp::categoryHSI()); add<&cfIncreaseLightness >(cs, COMPOSITE_INC_INTENSITY , i18n("Increase Intensity") , KoCompositeOp::categoryHSI()); add<&cfDecreaseLightness >(cs, COMPOSITE_DEC_INTENSITY , i18n("Decrease Intensity") , KoCompositeOp::categoryHSI()); add<&cfColor >(cs, COMPOSITE_COLOR_HSL , i18n("Color HSL") , KoCompositeOp::categoryHSL()); add<&cfHue >(cs, COMPOSITE_HUE_HSL , i18n("Hue HSL") , KoCompositeOp::categoryHSL()); add<&cfSaturation >(cs, COMPOSITE_SATURATION_HSL , i18n("Saturation HSL") , KoCompositeOp::categoryHSL()); add<&cfIncreaseSaturation >(cs, COMPOSITE_INC_SATURATION_HSL, i18n("Increase Saturation HSL"), KoCompositeOp::categoryHSL()); add<&cfDecreaseSaturation >(cs, COMPOSITE_DEC_SATURATION_HSL, i18n("Decrease Saturation HSL"), KoCompositeOp::categoryHSL()); add<&cfLightness >(cs, COMPOSITE_LIGHTNESS , i18n("Lightness") , KoCompositeOp::categoryHSL()); add<&cfIncreaseLightness >(cs, COMPOSITE_INC_LIGHTNESS , i18n("Increase Lightness") , KoCompositeOp::categoryHSL()); add<&cfDecreaseLightness >(cs, COMPOSITE_DEC_LIGHTNESS , i18n("Decrease Lightness") , KoCompositeOp::categoryHSL()); add<&cfColor >(cs, COMPOSITE_COLOR_HSV , i18n("Color HSV") , KoCompositeOp::categoryHSV()); add<&cfHue >(cs, COMPOSITE_HUE_HSV , i18n("Hue HSV") , KoCompositeOp::categoryHSV()); add<&cfSaturation >(cs, COMPOSITE_SATURATION_HSV , i18n("Saturation HSV") , KoCompositeOp::categoryHSV()); add<&cfIncreaseSaturation >(cs, COMPOSITE_INC_SATURATION_HSV, i18n("Increase Saturation HSV"), KoCompositeOp::categoryHSV()); add<&cfDecreaseSaturation >(cs, COMPOSITE_DEC_SATURATION_HSV, i18n("Decrease Saturation HSV"), KoCompositeOp::categoryHSV()); add<&cfLightness >(cs, COMPOSITE_VALUE , i18nc("HSV Value","Value") , KoCompositeOp::categoryHSV()); add<&cfIncreaseLightness >(cs, COMPOSITE_INC_VALUE , i18n("Increase Value") , KoCompositeOp::categoryHSV()); add<&cfDecreaseLightness >(cs, COMPOSITE_DEC_VALUE , i18n("Decrease Value") , KoCompositeOp::categoryHSV()); } }; template struct AddGeneralAlphaOps { static void add(KoColorSpace* cs) { Q_UNUSED(cs); } }; template struct AddGeneralAlphaOps { typedef float Arg; static const qint32 alpha_pos = Traits::alpha_pos; template static void add(KoColorSpace* cs, const QString& id, const QString& description, const QString& category) { cs->addCompositeOp(new KoCompositeOpGenericSCAlpha(cs, id, description, category)); } static void add(KoColorSpace* cs) { add<&cfAdditionSAI >(cs, COMPOSITE_LUMINOSITY_SAI , i18n("Luminosity/Shine (SAI)") , KoCompositeOp::categoryHSV()); } }; } /** * This function add to the colorspace all the composite ops defined by * the pigment library. */ template void addStandardCompositeOps(KoColorSpace* cs) { typedef typename _Traits_::channels_type channels_type; static const bool useGeneralOps = true; static const bool useRGBOps = (boost::is_base_of, _Traits_>::value || boost::is_base_of, _Traits_>::value); _Private::AddGeneralOps <_Traits_, useGeneralOps>::add(cs); _Private::AddRGBOps <_Traits_, useRGBOps >::add(cs); _Private::AddGeneralAlphaOps <_Traits_, useGeneralOps>::add(cs); } template KoCompositeOp* createAlphaDarkenCompositeOp(const KoColorSpace *cs) { return _Private::OptimizedOpsSelector<_Traits_>::createAlphaDarkenOp(cs); } #endif diff --git a/libs/pigment/compositeops/KoStreamedMath.h b/libs/pigment/compositeops/KoStreamedMath.h index 21de9686f7..4cc3116b0a 100644 --- a/libs/pigment/compositeops/KoStreamedMath.h +++ b/libs/pigment/compositeops/KoStreamedMath.h @@ -1,427 +1,427 @@ /* * Copyright (c) 2012 Dmitry Kazakov * * 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. */ #ifndef __KOSTREAMED_MATH_H #define __KOSTREAMED_MATH_H #if defined _MSC_VER // Lets shut up the "possible loss of data" and "forcing value to bool 'true' or 'false' #pragma warning ( push ) #pragma warning ( disable : 4244 ) #pragma warning ( disable : 4800 ) #endif #include #include #if defined _MSC_VER #pragma warning ( pop ) #endif #include #include #include #define BLOCKDEBUG 0 #if !defined _MSC_VER #pragma GCC diagnostic ignored "-Wcast-align" #endif template struct KoStreamedMath { using int_v = Vc::SimdArray; using uint_v = Vc::SimdArray; /** * Composes src into dst without using vector instructions */ template static void genericComposite_novector(const KoCompositeOp::ParameterInfo& params) { using namespace Arithmetic; const qint32 linearInc = pixelSize; qint32 srcLinearInc = params.srcRowStride ? pixelSize : 0; quint8* dstRowStart = params.dstRowStart; const quint8* maskRowStart = params.maskRowStart; const quint8* srcRowStart = params.srcRowStart; typename Compositor::ParamsWrapper paramsWrapper(params); for(quint32 r=params.rows; r>0; --r) { const quint8 *mask = maskRowStart; const quint8 *src = srcRowStart; quint8 *dst = dstRowStart; int blockRest = params.cols; for(int i = 0; i < blockRest; i++) { Compositor::template compositeOnePixelScalar(src, dst, mask, params.opacity, paramsWrapper); src += srcLinearInc; dst += linearInc; if (useMask) { mask++; } } srcRowStart += params.srcRowStride; dstRowStart += params.dstRowStride; if (useMask) { maskRowStart += params.maskRowStride; } } } template static void genericComposite32_novector(const KoCompositeOp::ParameterInfo& params) { genericComposite_novector(params); } template static void genericComposite128_novector(const KoCompositeOp::ParameterInfo& params) { genericComposite_novector(params); } static inline quint8 round_float_to_uint(float value) { return quint8(value + float(0.5)); } static inline quint8 lerp_mixed_u8_float(quint8 a, quint8 b, float alpha) { return round_float_to_uint(qint16(b - a) * alpha + a); } /** * Get a vector containing first Vc::float_v::size() values of mask. * Each source mask element is considered to be a 8-bit integer */ static inline Vc::float_v fetch_mask_8(const quint8 *data) { uint_v data_i(data); return Vc::simd_cast(int_v(data_i)); } /** * Get an alpha values from Vc::float_v::size() pixels 32-bit each * (4 channels, 8 bit per channel). The alpha value is considered * to be stored in the most significant byte of the pixel * * \p aligned controls whether the \p data is fetched using aligned * instruction or not. * 1) Fetching aligned data with unaligned instruction * degrades performance. * 2) Fetching unaligned data with aligned instruction * causes \#GP (General Protection Exception) */ template static inline Vc::float_v fetch_alpha_32(const quint8 *data) { uint_v data_i; if (aligned) { data_i.load((const quint32*)data, Vc::Aligned); } else { data_i.load((const quint32*)data, Vc::Unaligned); } return Vc::simd_cast(int_v(data_i >> 24)); } /** * Get color values from Vc::float_v::size() pixels 32-bit each * (4 channels, 8 bit per channel). The color data is considered * to be stored in the 3 least significant bytes of the pixel. * * \p aligned controls whether the \p data is fetched using aligned * instruction or not. * 1) Fetching aligned data with unaligned instruction * degrades performance. * 2) Fetching unaligned data with aligned instruction * causes \#GP (General Protection Exception) */ template static inline void fetch_colors_32(const quint8 *data, Vc::float_v &c1, Vc::float_v &c2, Vc::float_v &c3) { int_v data_i; if (aligned) { data_i.load((const quint32*)data, Vc::Aligned); } else { data_i.load((const quint32*)data, Vc::Unaligned); } const quint32 lowByteMask = 0xFF; uint_v mask(lowByteMask); c1 = Vc::simd_cast(int_v((data_i >> 16) & mask)); c2 = Vc::simd_cast(int_v((data_i >> 8) & mask)); c3 = Vc::simd_cast(int_v( data_i & mask)); } /** * Pack color and alpha values to Vc::float_v::size() pixels 32-bit each * (4 channels, 8 bit per channel). The color data is considered * to be stored in the 3 least significant bytes of the pixel, alpha - * in the most significant byte * * NOTE: \p data must be aligned pointer! */ static inline void write_channels_32(quint8 *data, Vc::float_v::AsArg alpha, Vc::float_v::AsArg c1, Vc::float_v::AsArg c2, Vc::float_v::AsArg c3) { /** * FIXME: make conversion float->int - * use methematical rounding + * use mathematical rounding */ const quint32 lowByteMask = 0xFF; // FIXME: Use single-instruction rounding + conversion // The achieve that we need to implement Vc::iRound() uint_v mask(lowByteMask); uint_v v1 = uint_v(int_v(Vc::round(alpha))) << 24; uint_v v2 = (uint_v(int_v(Vc::round(c1))) & mask) << 16; uint_v v3 = (uint_v(int_v(Vc::round(c2))) & mask) << 8; uint_v v4 = uint_v(int_v(Vc::round(c3))) & mask; v1 = v1 | v2; v3 = v3 | v4; (v1 | v3).store((quint32*)data, Vc::Aligned); } /** * Composes src pixels into dst pixles. Is optimized for 32-bit-per-pixel * colorspaces. Uses \p Compositor strategy parameter for doing actual * math of the composition */ template static void genericComposite(const KoCompositeOp::ParameterInfo& params) { using namespace Arithmetic; const int vectorSize = Vc::float_v::size(); const qint32 vectorInc = pixelSize * vectorSize; const qint32 linearInc = pixelSize; qint32 srcVectorInc = vectorInc; qint32 srcLinearInc = pixelSize; quint8* dstRowStart = params.dstRowStart; const quint8* maskRowStart = params.maskRowStart; const quint8* srcRowStart = params.srcRowStart; typename Compositor::ParamsWrapper paramsWrapper(params); if (!params.srcRowStride) { if (pixelSize == 4) { quint32 *buf = Vc::malloc(vectorSize); *((uint_v*)buf) = uint_v(*((const quint32*)params.srcRowStart)); srcRowStart = reinterpret_cast(buf); srcLinearInc = 0; srcVectorInc = 0; } else { quint8 *buf = Vc::malloc(vectorInc); quint8 *ptr = buf; for (int i = 0; i < vectorSize; i++) { memcpy(ptr, params.srcRowStart, pixelSize); ptr += pixelSize; } srcRowStart = buf; srcLinearInc = 0; srcVectorInc = 0; } } #if BLOCKDEBUG int totalBlockAlign = 0; int totalBlockAlignedVector = 0; int totalBlockUnalignedVector = 0; int totalBlockRest = 0; #endif for(quint32 r=params.rows; r>0; --r) { // Hint: Mask is allowed to be unaligned const quint8 *mask = maskRowStart; const quint8 *src = srcRowStart; quint8 *dst = dstRowStart; const int pixelsAlignmentMask = vectorSize * sizeof(float) - 1; uintptr_t srcPtrValue = reinterpret_cast(src); uintptr_t dstPtrValue = reinterpret_cast(dst); uintptr_t srcAlignment = srcPtrValue & pixelsAlignmentMask; uintptr_t dstAlignment = dstPtrValue & pixelsAlignmentMask; // Uncomment if facing problems with alignment: // Q_ASSERT_X(!(dstAlignment & 3), "Compositioning", // "Pixel data must be aligned on pixels borders!"); int blockAlign = params.cols; int blockAlignedVector = 0; int blockUnalignedVector = 0; int blockRest = 0; int *vectorBlock = srcAlignment == dstAlignment || !srcVectorInc ? &blockAlignedVector : &blockUnalignedVector; if (!dstAlignment) { blockAlign = 0; *vectorBlock = params.cols / vectorSize; blockRest = params.cols % vectorSize; } else if (params.cols > 2 * vectorSize) { blockAlign = (vectorInc - dstAlignment) / pixelSize; const int restCols = params.cols - blockAlign; if (restCols > 0) { *vectorBlock = restCols / vectorSize; blockRest = restCols % vectorSize; } else { blockAlign = params.cols; *vectorBlock = 0; blockRest = 0; } } #if BLOCKDEBUG totalBlockAlign += blockAlign; totalBlockAlignedVector += blockAlignedVector; totalBlockUnalignedVector += blockUnalignedVector; totalBlockRest += blockRest; #endif for(int i = 0; i < blockAlign; i++) { Compositor::template compositeOnePixelScalar(src, dst, mask, params.opacity, paramsWrapper); src += srcLinearInc; dst += linearInc; if(useMask) { mask++; } } for (int i = 0; i < blockAlignedVector; i++) { Compositor::template compositeVector(src, dst, mask, params.opacity, paramsWrapper); src += srcVectorInc; dst += vectorInc; if (useMask) { mask += vectorSize; } } for (int i = 0; i < blockUnalignedVector; i++) { Compositor::template compositeVector(src, dst, mask, params.opacity, paramsWrapper); src += srcVectorInc; dst += vectorInc; if (useMask) { mask += vectorSize; } } for(int i = 0; i < blockRest; i++) { Compositor::template compositeOnePixelScalar(src, dst, mask, params.opacity, paramsWrapper); src += srcLinearInc; dst += linearInc; if (useMask) { mask++; } } srcRowStart += params.srcRowStride; dstRowStart += params.dstRowStride; if (useMask) { maskRowStart += params.maskRowStride; } } #if BLOCKDEBUG dbgPigment << "I" << "rows:" << params.rows << "\tpad(S):" << totalBlockAlign << "\tbav(V):" << totalBlockAlignedVector << "\tbuv(V):" << totalBlockUnalignedVector << "\tres(S)" << totalBlockRest; // << srcAlignment << dstAlignment; #endif if (!params.srcRowStride) { Vc::free(reinterpret_cast(const_cast(srcRowStart))); } } template static void genericComposite32(const KoCompositeOp::ParameterInfo& params) { genericComposite(params); } template static void genericComposite128(const KoCompositeOp::ParameterInfo& params) { genericComposite(params); } }; namespace KoStreamedMathFunctions { template ALWAYS_INLINE void clearPixel(quint8* dst); template<> ALWAYS_INLINE void clearPixel<4>(quint8* dst) { quint32 *d = reinterpret_cast(dst); *d = 0; } template<> ALWAYS_INLINE void clearPixel<16>(quint8* dst) { quint64 *d = reinterpret_cast(dst); d[0] = 0; d[1] = 0; } template ALWAYS_INLINE void copyPixel(const quint8 *src, quint8* dst); template<> ALWAYS_INLINE void copyPixel<4>(const quint8 *src, quint8* dst) { const quint32 *s = reinterpret_cast(src); quint32 *d = reinterpret_cast(dst); *d = *s; } template<> ALWAYS_INLINE void copyPixel<16>(const quint8 *src, quint8* dst) { const quint64 *s = reinterpret_cast(src); quint64 *d = reinterpret_cast(dst); d[0] = s[0]; d[1] = s[1]; } } #endif /* __KOSTREAMED_MATH_H */ diff --git a/libs/pigment/tests/CCSGraph.cpp b/libs/pigment/tests/CCSGraph.cpp index 55f43011c2..6a574af35e 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 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)"; + 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.graphviz.org)"; } } else { errorPigment << "Unknown output type : " << outputType; exit(EXIT_FAILURE); } } diff --git a/libs/ui/KisApplication.cpp b/libs/ui/KisApplication.cpp index 4dad1894e1..15f9e03b2b 100644 --- a/libs/ui/KisApplication.cpp +++ b/libs/ui/KisApplication.cpp @@ -1,874 +1,874 @@ /* * Copyright (C) 1998, 1999 Torben Weis * Copyright (C) 2012 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 "KisApplication.h" #include #ifdef Q_OS_WIN #include #include #endif #ifdef Q_OS_MACOS #include "osx.h" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "KoConfig.h" #include #include #include #include "thememanager.h" #include "KisPrintJob.h" #include "KisDocument.h" #include "KisMainWindow.h" #include "KisAutoSaveRecoveryDialog.h" #include "KisPart.h" #include #include "kis_md5_generator.h" #include "kis_splash_screen.h" #include "kis_config.h" #include "flake/kis_shape_selection.h" #include #include #include #include #include #include #include #include "kisexiv2/kis_exiv2.h" #include "KisApplicationArguments.h" #include #include "kis_action_registry.h" #include #include #include #include "kis_image_barrier_locker.h" #include "opengl/kis_opengl.h" #include "kis_spin_box_unit_manager.h" #include "kis_document_aware_spin_box_unit_manager.h" #include "KisViewManager.h" #include "kis_workspace_resource.h" #include #include #include "widgets/KisScreenColorPicker.h" #include "KisDlgInternalColorSelector.h" #include #include namespace { const QTime appStartTime(QTime::currentTime()); } class KisApplication::Private { public: Private() {} QPointer splashScreen; KisAutoSaveRecoveryDialog *autosaveDialog {0}; QPointer mainWindow; // The first mainwindow we create on startup bool batchRun {false}; }; class KisApplication::ResetStarting { public: ResetStarting(KisSplashScreen *splash, int fileCount) : m_splash(splash) , m_fileCount(fileCount) { } ~ResetStarting() { if (m_splash) { m_splash->hide(); } } QPointer m_splash; int m_fileCount; }; KisApplication::KisApplication(const QString &key, int &argc, char **argv) : QtSingleApplication(key, argc, argv) , d(new Private) { #ifdef Q_OS_MACOS setMouseCoalescingEnabled(false); #endif QCoreApplication::addLibraryPath(QCoreApplication::applicationDirPath()); setApplicationDisplayName("Krita"); setApplicationName("krita"); // Note: Qt docs suggest we set this, but if we do, we get resource paths of the form of krita/krita, which is weird. // setOrganizationName("krita"); setOrganizationDomain("krita.org"); QString version = KritaVersionWrapper::versionString(true); setApplicationVersion(version); setWindowIcon(KisIconUtils::loadIcon("krita")); if (qgetenv("KRITA_NO_STYLE_OVERRIDE").isEmpty()) { QStringList styles = QStringList() /*<< "breeze"*/ << "fusion" << "plastique"; if (!styles.contains(style()->objectName().toLower())) { Q_FOREACH (const QString & style, styles) { if (!setStyle(style)) { qDebug() << "No" << style << "available."; } else { qDebug() << "Set style" << style; break; } } } } else { qDebug() << "Style override disabled, using" << style()->objectName(); } KisOpenGL::initialize(); } #if defined(Q_OS_WIN) && defined(ENV32BIT) typedef BOOL (WINAPI *LPFN_ISWOW64PROCESS) (HANDLE, PBOOL); LPFN_ISWOW64PROCESS fnIsWow64Process; BOOL isWow64() { BOOL bIsWow64 = FALSE; //IsWow64Process is not available on all supported versions of Windows. //Use GetModuleHandle to get a handle to the DLL that contains the function //and GetProcAddress to get a pointer to the function if available. fnIsWow64Process = (LPFN_ISWOW64PROCESS) GetProcAddress( GetModuleHandle(TEXT("kernel32")),"IsWow64Process"); if(0 != fnIsWow64Process) { if (!fnIsWow64Process(GetCurrentProcess(),&bIsWow64)) { //handle error } } return bIsWow64; } #endif void KisApplication::initializeGlobals(const KisApplicationArguments &args) { int dpiX = args.dpiX(); int dpiY = args.dpiY(); if (dpiX > 0 && dpiY > 0) { KoDpi::setDPI(dpiX, dpiY); } } void KisApplication::addResourceTypes() { // qDebug() << "addResourceTypes();"; // All Krita's resource types KoResourcePaths::addResourceType("markers", "data", "/styles/"); KoResourcePaths::addResourceType("kis_pics", "data", "/pics/"); KoResourcePaths::addResourceType("kis_images", "data", "/images/"); KoResourcePaths::addResourceType("metadata_schema", "data", "/metadata/schemas/"); KoResourcePaths::addResourceType("kis_brushes", "data", "/brushes/"); KoResourcePaths::addResourceType("kis_taskset", "data", "/taskset/"); KoResourcePaths::addResourceType("kis_taskset", "data", "/taskset/"); KoResourcePaths::addResourceType("gmic_definitions", "data", "/gmic/"); KoResourcePaths::addResourceType("kis_resourcebundles", "data", "/bundles/"); KoResourcePaths::addResourceType("kis_defaultpresets", "data", "/defaultpresets/"); KoResourcePaths::addResourceType("kis_paintoppresets", "data", "/paintoppresets/"); KoResourcePaths::addResourceType("kis_workspaces", "data", "/workspaces/"); KoResourcePaths::addResourceType("kis_windowlayouts", "data", "/windowlayouts/"); KoResourcePaths::addResourceType("kis_sessions", "data", "/sessions/"); KoResourcePaths::addResourceType("psd_layer_style_collections", "data", "/asl"); KoResourcePaths::addResourceType("ko_patterns", "data", "/patterns/", true); KoResourcePaths::addResourceType("ko_gradients", "data", "/gradients/"); KoResourcePaths::addResourceType("ko_gradients", "data", "/gradients/", true); KoResourcePaths::addResourceType("ko_palettes", "data", "/palettes/", true); KoResourcePaths::addResourceType("kis_shortcuts", "data", "/shortcuts/"); KoResourcePaths::addResourceType("kis_actions", "data", "/actions"); KoResourcePaths::addResourceType("kis_actions", "data", "/pykrita"); KoResourcePaths::addResourceType("icc_profiles", "data", "/color/icc"); KoResourcePaths::addResourceType("icc_profiles", "data", "/profiles/"); KoResourcePaths::addResourceType("ko_effects", "data", "/effects/"); KoResourcePaths::addResourceType("tags", "data", "/tags/"); KoResourcePaths::addResourceType("templates", "data", "/templates"); KoResourcePaths::addResourceType("pythonscripts", "data", "/pykrita"); KoResourcePaths::addResourceType("symbols", "data", "/symbols"); KoResourcePaths::addResourceType("preset_icons", "data", "/preset_icons"); KoResourcePaths::addResourceType("ko_gamutmasks", "data", "/gamutmasks/", true); // // Extra directories to look for create resources. (Does anyone actually use that anymore?) // KoResourcePaths::addResourceDir("ko_gradients", "/usr/share/create/gradients/gimp"); // KoResourcePaths::addResourceDir("ko_gradients", QDir::homePath() + QString("/.create/gradients/gimp")); // KoResourcePaths::addResourceDir("ko_patterns", "/usr/share/create/patterns/gimp"); // KoResourcePaths::addResourceDir("ko_patterns", QDir::homePath() + QString("/.create/patterns/gimp")); // KoResourcePaths::addResourceDir("kis_brushes", "/usr/share/create/brushes/gimp"); // KoResourcePaths::addResourceDir("kis_brushes", QDir::homePath() + QString("/.create/brushes/gimp")); // KoResourcePaths::addResourceDir("ko_palettes", "/usr/share/create/swatches"); // KoResourcePaths::addResourceDir("ko_palettes", QDir::homePath() + QString("/.create/swatches")); // Make directories for all resources we can save, and tags QDir d; d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/tags/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/asl/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/bundles/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/brushes/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/gradients/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/paintoppresets/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/palettes/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/patterns/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/taskset/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/workspaces/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/input/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/pykrita/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/symbols/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/color-schemes/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/preset_icons/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/preset_icons/tool_icons/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/preset_icons/emblem_icons/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/gamutmasks/"); } void KisApplication::loadResources() { // qDebug() << "loadResources();"; setSplashScreenLoadingText(i18n("Loading Resources...")); processEvents(); KoResourceServerProvider::instance(); setSplashScreenLoadingText(i18n("Loading Brush Presets...")); processEvents(); KisResourceServerProvider::instance(); setSplashScreenLoadingText(i18n("Loading Brushes...")); processEvents(); KisBrushServer::instance()->brushServer(); setSplashScreenLoadingText(i18n("Loading Bundles...")); processEvents(); KisResourceBundleServerProvider::instance(); } void KisApplication::loadResourceTags() { // qDebug() << "loadResourceTags()"; KoResourceServerProvider::instance()->patternServer()->loadTags(); KoResourceServerProvider::instance()->gradientServer()->loadTags(); KoResourceServerProvider::instance()->paletteServer()->loadTags(); KoResourceServerProvider::instance()->svgSymbolCollectionServer()->loadTags(); KisBrushServer::instance()->brushServer()->loadTags(); KisResourceServerProvider::instance()->workspaceServer()->loadTags(); KisResourceServerProvider::instance()->layerStyleCollectionServer()->loadTags(); KisResourceBundleServerProvider::instance()->resourceBundleServer()->loadTags(); KisResourceServerProvider::instance()->paintOpPresetServer()->loadTags(); KisResourceServerProvider::instance()->paintOpPresetServer()->clearOldSystemTags(); } void KisApplication::loadPlugins() { // qDebug() << "loadPlugins();"; KoShapeRegistry* r = KoShapeRegistry::instance(); r->add(new KisShapeSelectionFactory()); KisActionRegistry::instance(); KisFilterRegistry::instance(); KisGeneratorRegistry::instance(); KisPaintOpRegistry::instance(); KoColorSpaceRegistry::instance(); } void KisApplication::loadGuiPlugins() { // qDebug() << "loadGuiPlugins();"; // Load the krita-specific tools setSplashScreenLoadingText(i18n("Loading Plugins for Krita/Tool...")); processEvents(); // qDebug() << "loading tools"; KoPluginLoader::instance()->load(QString::fromLatin1("Krita/Tool"), QString::fromLatin1("[X-Krita-Version] == 28")); // Load dockers setSplashScreenLoadingText(i18n("Loading Plugins for Krita/Dock...")); processEvents(); // qDebug() << "loading dockers"; KoPluginLoader::instance()->load(QString::fromLatin1("Krita/Dock"), QString::fromLatin1("[X-Krita-Version] == 28")); // XXX_EXIV: make the exiv io backends real plugins setSplashScreenLoadingText(i18n("Loading Plugins Exiv/IO...")); processEvents(); // qDebug() << "loading exiv2"; KisExiv2::initialize(); } bool KisApplication::start(const KisApplicationArguments &args) { KisConfig cfg(false); #if defined(Q_OS_WIN) #ifdef ENV32BIT if (isWow64() && !cfg.readEntry("WarnedAbout32Bits", false)) { QMessageBox::information(0, i18nc("@title:window", "Krita: Warning"), i18n("You are running a 32 bits build on a 64 bits Windows.\n" "This is not recommended.\n" "Please download and install the x64 build instead.")); cfg.writeEntry("WarnedAbout32Bits", true); } #endif #endif QString opengl = cfg.canvasState(); if (opengl == "OPENGL_NOT_TRIED" ) { cfg.setCanvasState("TRY_OPENGL"); } else if (opengl != "OPENGL_SUCCESS" && opengl != "TRY_OPENGL") { cfg.setCanvasState("OPENGL_FAILED"); } setSplashScreenLoadingText(i18n("Initializing Globals")); processEvents(); initializeGlobals(args); const bool doNewImage = args.doNewImage(); const bool doTemplate = args.doTemplate(); const bool exportAs = args.exportAs(); const bool exportSequence = args.exportSequence(); const QString exportFileName = args.exportFileName(); d->batchRun = (exportAs || exportSequence || !exportFileName.isEmpty()); const bool needsMainWindow = (!exportAs && !exportSequence); // only show the mainWindow when no command-line mode option is passed bool showmainWindow = (!exportAs && !exportSequence); // would be !batchRun; const bool showSplashScreen = !d->batchRun && qEnvironmentVariableIsEmpty("NOSPLASH"); if (showSplashScreen && d->splashScreen) { d->splashScreen->show(); d->splashScreen->repaint(); processEvents(); } KoHashGeneratorProvider::instance()->setGenerator("MD5", new KisMD5Generator()); KConfigGroup group(KSharedConfig::openConfig(), "theme"); Digikam::ThemeManager themeManager; themeManager.setCurrentTheme(group.readEntry("Theme", "Krita dark")); ResetStarting resetStarting(d->splashScreen, args.filenames().count()); // remove the splash when done Q_UNUSED(resetStarting); // Make sure we can save resources and tags setSplashScreenLoadingText(i18n("Adding resource types")); processEvents(); addResourceTypes(); // Load the plugins loadPlugins(); // Load all resources loadResources(); // Load all the tags loadResourceTags(); // Load the gui plugins loadGuiPlugins(); KisPart *kisPart = KisPart::instance(); if (needsMainWindow) { // show a mainWindow asap, if we want that setSplashScreenLoadingText(i18n("Loading Main Window...")); processEvents(); bool sessionNeeded = true; auto sessionMode = cfg.sessionOnStartup(); if (!args.session().isEmpty()) { sessionNeeded = !kisPart->restoreSession(args.session()); } else if (sessionMode == KisConfig::SOS_ShowSessionManager) { showmainWindow = false; sessionNeeded = false; kisPart->showSessionManager(); } else if (sessionMode == KisConfig::SOS_PreviousSession) { KConfigGroup sessionCfg = KSharedConfig::openConfig()->group("session"); const QString &sessionName = sessionCfg.readEntry("previousSession"); sessionNeeded = !kisPart->restoreSession(sessionName); } if (sessionNeeded) { kisPart->startBlankSession(); } if (!args.windowLayout().isEmpty()) { KoResourceServer * rserver = KisResourceServerProvider::instance()->windowLayoutServer(); KisWindowLayoutResource* windowLayout = rserver->resourceByName(args.windowLayout()); if (windowLayout) { windowLayout->applyLayout(); } } if (showmainWindow) { d->mainWindow = kisPart->currentMainwindow(); if (!args.workspace().isEmpty()) { KoResourceServer * rserver = KisResourceServerProvider::instance()->workspaceServer(); KisWorkspaceResource* workspace = rserver->resourceByName(args.workspace()); if (workspace) { d->mainWindow->restoreWorkspace(workspace); } } if (args.canvasOnly()) { d->mainWindow->viewManager()->switchCanvasOnly(true); } if (args.fullScreen()) { d->mainWindow->showFullScreen(); } } else { d->mainWindow = kisPart->createMainWindow(); } } short int numberOfOpenDocuments = 0; // number of documents open // Check for autosave files that can be restored, if we're not running a batchrun (test) if (!d->batchRun) { checkAutosaveFiles(); } setSplashScreenLoadingText(QString()); // done loading, so clear out label processEvents(); //configure the unit manager KisSpinBoxUnitManagerFactory::setDefaultUnitManagerBuilder(new KisDocumentAwareSpinBoxUnitManagerBuilder()); connect(this, &KisApplication::aboutToQuit, &KisSpinBoxUnitManagerFactory::clearUnitManagerBuilder); //ensure the builder is destroyed when the application leave. //the new syntax slot syntax allow to connect to a non q_object static method. // Create a new image, if needed if (doNewImage) { KisDocument *doc = args.image(); if (doc) { kisPart->addDocument(doc); d->mainWindow->addViewAndNotifyLoadingCompleted(doc); } } // Get the command line arguments which we have to parse int argsCount = args.filenames().count(); if (argsCount > 0) { // Loop through arguments for (int argNumber = 0; argNumber < argsCount; argNumber++) { QString fileName = args.filenames().at(argNumber); // are we just trying to open a template? if (doTemplate) { // called in mix with batch options? ignore and silently skip if (d->batchRun) { continue; } if (createNewDocFromTemplate(fileName, d->mainWindow)) { ++numberOfOpenDocuments; } // now try to load } else { if (exportAs) { QString outputMimetype = KisMimeDatabase::mimeTypeForFile(exportFileName, false); if (outputMimetype == "application/octetstream") { dbgKrita << i18n("Mimetype not found, try using the -mimetype option") << endl; return false; } KisDocument *doc = kisPart->createDocument(); doc->setFileBatchMode(d->batchRun); bool result = doc->openUrl(QUrl::fromLocalFile(fileName)); if (!result) { errKrita << "Could not load " << fileName << ":" << doc->errorMessage(); QTimer::singleShot(0, this, SLOT(quit())); return false; } if (exportFileName.isEmpty()) { errKrita << "Export destination is not specified for" << fileName << "Please specify export destination with --export-filename option"; QTimer::singleShot(0, this, SLOT(quit())); return false; } qApp->processEvents(); // For vector layers to be updated doc->setFileBatchMode(true); if (!doc->exportDocumentSync(QUrl::fromLocalFile(exportFileName), outputMimetype.toLatin1())) { errKrita << "Could not export " << fileName << "to" << exportFileName << ":" << doc->errorMessage(); } QTimer::singleShot(0, this, SLOT(quit())); return true; } else if (exportSequence) { KisDocument *doc = kisPart->createDocument(); doc->setFileBatchMode(d->batchRun); doc->openUrl(QUrl::fromLocalFile(fileName)); qApp->processEvents(); // For vector layers to be updated if (!doc->image()->animationInterface()->hasAnimation()) { errKrita << "This file has no animation." << endl; QTimer::singleShot(0, this, SLOT(quit())); return false; } doc->setFileBatchMode(true); int sequenceStart = 0; KisAsyncAnimationFramesSaveDialog exporter(doc->image(), doc->image()->animationInterface()->fullClipRange(), exportFileName, sequenceStart, 0); exporter.setBatchMode(d->batchRun); KisAsyncAnimationFramesSaveDialog::Result result = exporter.regenerateRange(0); if (result == KisAsyncAnimationFramesSaveDialog::RenderFailed) { errKrita << i18n("Failed to render animation frames!") << endl; } QTimer::singleShot(0, this, SLOT(quit())); return true; } else if (d->mainWindow) { if (fileName.endsWith(".bundle")) { d->mainWindow->installBundle(fileName); } else { KisMainWindow::OpenFlags flags = d->batchRun ? KisMainWindow::BatchMode : KisMainWindow::None; if (d->mainWindow->openDocument(QUrl::fromLocalFile(fileName), flags)) { // Normal case, success numberOfOpenDocuments++; } } } } } } // fixes BUG:369308 - Krita crashing on splash screen when loading. // trying to open a file before Krita has loaded can cause it to hang and crash if (d->splashScreen) { d->splashScreen->displayLinks(true); d->splashScreen->displayRecentFiles(true); } // not calling this before since the program will quit there. return true; } KisApplication::~KisApplication() { } void KisApplication::setSplashScreen(QWidget *splashScreen) { d->splashScreen = qobject_cast(splashScreen); } void KisApplication::setSplashScreenLoadingText(QString textToLoad) { if (d->splashScreen) { //d->splashScreen->loadingLabel->setText(textToLoad); d->splashScreen->setLoadingText(textToLoad); d->splashScreen->repaint(); } } void KisApplication::hideSplashScreen() { if (d->splashScreen) { // hide the splashscreen to see the dialog d->splashScreen->hide(); } } bool KisApplication::notify(QObject *receiver, QEvent *event) { try { return QApplication::notify(receiver, event); } catch (std::exception &e) { qWarning("Error %s sending event %i to object %s", e.what(), event->type(), qPrintable(receiver->objectName())); } catch (...) { qWarning("Error sending event %i to object %s", event->type(), qPrintable(receiver->objectName())); } return false; } void KisApplication::remoteArguments(QByteArray message, QObject *socket) { Q_UNUSED(socket); // check if we have any mainwindow KisMainWindow *mw = qobject_cast(qApp->activeWindow()); if (!mw) { mw = KisPart::instance()->mainWindows().first(); } if (!mw) { return; } KisApplicationArguments args = KisApplicationArguments::deserialize(message); const bool doTemplate = args.doTemplate(); const int argsCount = args.filenames().count(); if (argsCount > 0) { // Loop through arguments for (int argNumber = 0; argNumber < argsCount; ++argNumber) { QString filename = args.filenames().at(argNumber); // are we just trying to open a template? if (doTemplate) { createNewDocFromTemplate(filename, mw); } else if (QFile(filename).exists()) { KisMainWindow::OpenFlags flags = d->batchRun ? KisMainWindow::BatchMode : KisMainWindow::None; mw->openDocument(QUrl::fromLocalFile(filename), flags); } } } } void KisApplication::fileOpenRequested(const QString &url) { KisMainWindow *mainWindow = KisPart::instance()->mainWindows().first(); if (mainWindow) { KisMainWindow::OpenFlags flags = d->batchRun ? KisMainWindow::BatchMode : KisMainWindow::None; mainWindow->openDocument(QUrl::fromLocalFile(url), flags); } } void KisApplication::checkAutosaveFiles() { if (d->batchRun) return; #ifdef Q_OS_WIN QDir dir = QDir::temp(); #else QDir dir = QDir::home(); #endif // Check for autosave files from a previous run. There can be several, and // we want to offer a restore for every one. Including a nice thumbnail! // Hidden autosave files QStringList filters = QStringList() << QString(".krita-*-*-autosave.kra"); // all autosave files for our application QStringList autosaveFiles = dir.entryList(filters, QDir::Files | QDir::Hidden); - // Visibile autosave files + // Visible autosave files filters = QStringList() << QString("krita-*-*-autosave.kra"); autosaveFiles += dir.entryList(filters, QDir::Files); // Allow the user to make their selection if (autosaveFiles.size() > 0) { if (d->splashScreen) { // hide the splashscreen to see the dialog d->splashScreen->hide(); } d->autosaveDialog = new KisAutoSaveRecoveryDialog(autosaveFiles, activeWindow()); QDialog::DialogCode result = (QDialog::DialogCode) d->autosaveDialog->exec(); if (result == QDialog::Accepted) { QStringList filesToRecover = d->autosaveDialog->recoverableFiles(); Q_FOREACH (const QString &autosaveFile, autosaveFiles) { if (!filesToRecover.contains(autosaveFile)) { QFile::remove(dir.absolutePath() + "/" + autosaveFile); } } autosaveFiles = filesToRecover; } else { autosaveFiles.clear(); } if (autosaveFiles.size() > 0) { QList autosaveUrls; Q_FOREACH (const QString &autoSaveFile, autosaveFiles) { const QUrl url = QUrl::fromLocalFile(dir.absolutePath() + QLatin1Char('/') + autoSaveFile); autosaveUrls << url; } if (d->mainWindow) { Q_FOREACH (const QUrl &url, autosaveUrls) { KisMainWindow::OpenFlags flags = d->batchRun ? KisMainWindow::BatchMode : KisMainWindow::None; d->mainWindow->openDocument(url, flags | KisMainWindow::RecoveryFile); } } } // cleanup delete d->autosaveDialog; d->autosaveDialog = nullptr; } } bool KisApplication::createNewDocFromTemplate(const QString &fileName, KisMainWindow *mainWindow) { QString templatePath; const QUrl templateUrl = QUrl::fromLocalFile(fileName); if (QFile::exists(fileName)) { templatePath = templateUrl.toLocalFile(); dbgUI << "using full path..."; } else { QString desktopName(fileName); const QString templatesResourcePath = QStringLiteral("templates/"); QStringList paths = KoResourcePaths::findAllResources("data", templatesResourcePath + "*/" + desktopName); if (paths.isEmpty()) { paths = KoResourcePaths::findAllResources("data", templatesResourcePath + desktopName); } if (paths.isEmpty()) { QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("No template found for: %1", desktopName)); } else if (paths.count() > 1) { QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("Too many templates found for: %1", desktopName)); } else { templatePath = paths.at(0); } } if (!templatePath.isEmpty()) { QUrl templateBase; templateBase.setPath(templatePath); KDesktopFile templateInfo(templatePath); QString templateName = templateInfo.readUrl(); QUrl templateURL; templateURL.setPath(templateBase.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path() + '/' + templateName); if (templateURL.scheme().isEmpty()) { templateURL.setScheme("file"); } KisMainWindow::OpenFlags batchFlags = d->batchRun ? KisMainWindow::BatchMode : KisMainWindow::None; if (mainWindow->openDocument(templateURL, KisMainWindow::Import | batchFlags)) { dbgUI << "Template loaded..."; return true; } else { QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("Template %1 failed to load.", templateURL.toDisplayString())); } } return false; } void KisApplication::clearConfig() { KIS_ASSERT_RECOVER_RETURN(qApp->thread() == QThread::currentThread()); KSharedConfigPtr config = KSharedConfig::openConfig(); // find user settings file bool createDir = false; QString kritarcPath = KoResourcePaths::locateLocal("config", "kritarc", createDir); QFile configFile(kritarcPath); if (configFile.exists()) { // clear file if (configFile.open(QFile::WriteOnly)) { configFile.close(); } else { QMessageBox::warning(0, i18nc("@title:window", "Krita"), i18n("Failed to clear %1\n\n" "Please make sure no other program is using the file and try again.", kritarcPath), QMessageBox::Ok, QMessageBox::Ok); } } // reload from disk; with the user file settings cleared, // this should load any default configuration files shipping with the program config->reparseConfiguration(); config->sync(); } void KisApplication::askClearConfig() { Qt::KeyboardModifiers mods = QApplication::queryKeyboardModifiers(); bool askClearConfig = (mods & Qt::ControlModifier) && (mods & Qt::ShiftModifier) && (mods & Qt::AltModifier); if (askClearConfig) { bool ok = QMessageBox::question(0, i18nc("@title:window", "Krita"), i18n("Do you want to clear the settings file?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes; if (ok) { clearConfig(); } } } diff --git a/libs/ui/KisDocument.cpp b/libs/ui/KisDocument.cpp index 7f74ee868b..1f9cbb5bd9 100644 --- a/libs/ui/KisDocument.cpp +++ b/libs/ui/KisDocument.cpp @@ -1,2258 +1,2258 @@ /* This file is part of the Krita project * * Copyright (C) 2014 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 "KisMainWindow.h" // XXX: remove #include // XXX: remove #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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Krita Image #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_layer_utils.h" // Local #include "KisViewManager.h" #include "kis_clipboard.h" #include "widgets/kis_custom_image_widget.h" #include "canvas/kis_canvas2.h" #include "flake/kis_shape_controller.h" #include "kis_statusbar.h" #include "widgets/kis_progress_widget.h" #include "kis_canvas_resource_provider.h" #include "KisResourceServerProvider.h" #include "kis_node_manager.h" #include "KisPart.h" #include "KisApplication.h" #include "KisDocument.h" #include "KisImportExportManager.h" #include "KisView.h" #include "kis_grid_config.h" #include "kis_guides_config.h" #include "kis_image_barrier_lock_adapter.h" #include "KisReferenceImagesLayer.h" #include #include "kis_config_notifier.h" #include "kis_async_action_feedback.h" #include "KisCloneDocumentStroke.h" #include #include #include "kis_simple_stroke_strategy.h" // Define the protocol used here for embedded documents' URL // This used to "store" but QUrl didn't like it, // so let's simply make it "tar" ! #define STORE_PROTOCOL "tar" // The internal path is a hack to make QUrl happy and for document children #define INTERNAL_PROTOCOL "intern" #define INTERNAL_PREFIX "intern:/" // Warning, keep it sync in koStore.cc #include using namespace std; namespace { constexpr int errorMessageTimeout = 5000; constexpr int successMessageTimeout = 1000; } /********************************************************** * * KisDocument * **********************************************************/ //static QString KisDocument::newObjectName() { static int s_docIFNumber = 0; QString name; name.setNum(s_docIFNumber++); name.prepend("document_"); return name; } class UndoStack : public KUndo2Stack { public: UndoStack(KisDocument *doc) : KUndo2Stack(doc), m_doc(doc) { } void setIndex(int idx) override { KisImageWSP image = this->image(); image->requestStrokeCancellation(); if(image->tryBarrierLock()) { KUndo2Stack::setIndex(idx); image->unlock(); } } void notifySetIndexChangedOneCommand() override { KisImageWSP image = this->image(); image->unlock(); /** * Some very weird commands may emit blocking signals to * the GUI (e.g. KisGuiContextCommand). Here is the best thing * we can do to avoid the deadlock */ while(!image->tryBarrierLock()) { QApplication::processEvents(); } } void undo() override { KisImageWSP image = this->image(); image->requestUndoDuringStroke(); if (image->tryUndoUnfinishedLod0Stroke() == UNDO_OK) { return; } if(image->tryBarrierLock()) { KUndo2Stack::undo(); image->unlock(); } } void redo() override { KisImageWSP image = this->image(); if(image->tryBarrierLock()) { KUndo2Stack::redo(); image->unlock(); } } private: KisImageWSP image() { KisImageWSP currentImage = m_doc->image(); Q_ASSERT(currentImage); return currentImage; } private: KisDocument *m_doc; }; class Q_DECL_HIDDEN KisDocument::Private { public: Private(KisDocument *_q) : q(_q) , docInfo(new KoDocumentInfo(_q)) // deleted by QObject , importExportManager(new KisImportExportManager(_q)) // deleted manually , autoSaveTimer(new QTimer(_q)) , undoStack(new UndoStack(_q)) // deleted by QObject , m_bAutoDetectedMime(false) , modified(false) , readwrite(true) , firstMod(QDateTime::currentDateTime()) , lastMod(firstMod) , nserver(new KisNameServer(1)) , imageIdleWatcher(2000 /*ms*/) , globalAssistantsColor(KisConfig(true).defaultAssistantsColor()) , savingLock(&savingMutex) , batchMode(false) { if (QLocale().measurementSystem() == QLocale::ImperialSystem) { unit = KoUnit::Inch; } else { unit = KoUnit::Centimeter; } } Private(const Private &rhs, KisDocument *_q) : q(_q) , docInfo(new KoDocumentInfo(*rhs.docInfo, _q)) , importExportManager(new KisImportExportManager(_q)) , autoSaveTimer(new QTimer(_q)) , undoStack(new UndoStack(_q)) , nserver(new KisNameServer(*rhs.nserver)) , preActivatedNode(0) // the node is from another hierarchy! , imageIdleWatcher(2000 /*ms*/) , savingLock(&savingMutex) { copyFromImpl(rhs, _q, CONSTRUCT); } ~Private() { // Don't delete m_d->shapeController because it's in a QObject hierarchy. delete nserver; } KisDocument *q = 0; KoDocumentInfo *docInfo = 0; KoUnit unit; KisImportExportManager *importExportManager = 0; // The filter-manager to use when loading/saving [for the options] QByteArray mimeType; // The actual mimetype of the document QByteArray outputMimeType; // The mimetype to use when saving QTimer *autoSaveTimer; QString lastErrorMessage; // see openFile() QString lastWarningMessage; int autoSaveDelay = 300; // in seconds, 0 to disable. bool modifiedAfterAutosave = false; bool isAutosaving = false; bool disregardAutosaveFailure = false; int autoSaveFailureCount = 0; KUndo2Stack *undoStack = 0; KisGuidesConfig guidesConfig; KisMirrorAxisConfig mirrorAxisConfig; bool m_bAutoDetectedMime = false; // whether the mimetype in the arguments was detected by the part itself QUrl m_url; // local url - the one displayed to the user. QString m_file; // Local file - the only one the part implementation should deal with. QMutex savingMutex; bool modified = false; bool readwrite = false; QDateTime firstMod; QDateTime lastMod; KisNameServer *nserver; KisImageSP image; KisImageSP savingImage; KisNodeWSP preActivatedNode; KisShapeController* shapeController = 0; KoShapeController* koShapeController = 0; KisIdleWatcher imageIdleWatcher; QScopedPointer imageIdleConnection; QList assistants; QColor globalAssistantsColor; QList paletteList; bool ownsPaletteList = false; KisGridConfig gridConfig; StdLockableWrapper savingLock; bool modifiedWhileSaving = false; QScopedPointer backgroundSaveDocument; QPointer savingUpdater; QFuture childSavingFuture; KritaUtils::ExportFileJob backgroundSaveJob; bool isRecovered = false; bool batchMode { false }; void syncDecorationsWrapperLayerState(); void setImageAndInitIdleWatcher(KisImageSP _image) { image = _image; imageIdleWatcher.setTrackedImage(image); if (image) { imageIdleConnection.reset( new KisSignalAutoConnection( &imageIdleWatcher, SIGNAL(startedIdleMode()), image.data(), SLOT(explicitRegenerateLevelOfDetail()))); } } void copyFrom(const Private &rhs, KisDocument *q); void copyFromImpl(const Private &rhs, KisDocument *q, KisDocument::CopyPolicy policy); /// clones the palette list oldList /// the ownership of the returned KoColorSet * belongs to the caller QList clonePaletteList(const QList &oldList); class StrippedSafeSavingLocker; }; void KisDocument::Private::syncDecorationsWrapperLayerState() { if (!this->image) return; KisImageSP image = this->image; KisDecorationsWrapperLayerSP decorationsLayer = KisLayerUtils::findNodeByType(image->root()); const bool needsDecorationsWrapper = gridConfig.showGrid() || (guidesConfig.showGuides() && guidesConfig.hasGuides()) || !assistants.isEmpty(); struct SyncDecorationsWrapperStroke : public KisSimpleStrokeStrategy { SyncDecorationsWrapperStroke(KisDocument *document, bool needsDecorationsWrapper) : KisSimpleStrokeStrategy("sync-decorations-wrapper", kundo2_noi18n("start-isolated-mode")), m_document(document), m_needsDecorationsWrapper(needsDecorationsWrapper) { this->enableJob(JOB_INIT, true, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); setClearsRedoOnStart(false); } void initStrokeCallback() { KisDecorationsWrapperLayerSP decorationsLayer = KisLayerUtils::findNodeByType(m_document->image()->root()); if (m_needsDecorationsWrapper && !decorationsLayer) { m_document->image()->addNode(new KisDecorationsWrapperLayer(m_document)); } else if (!m_needsDecorationsWrapper && decorationsLayer) { m_document->image()->removeNode(decorationsLayer); } } private: KisDocument *m_document = 0; bool m_needsDecorationsWrapper = false; }; KisStrokeId id = image->startStroke(new SyncDecorationsWrapperStroke(q, needsDecorationsWrapper)); image->endStroke(id); } void KisDocument::Private::copyFrom(const Private &rhs, KisDocument *q) { copyFromImpl(rhs, q, KisDocument::REPLACE); } void KisDocument::Private::copyFromImpl(const Private &rhs, KisDocument *q, KisDocument::CopyPolicy policy) { if (policy == REPLACE) { delete docInfo; } docInfo = (new KoDocumentInfo(*rhs.docInfo, q)); unit = rhs.unit; mimeType = rhs.mimeType; outputMimeType = rhs.outputMimeType; if (policy == REPLACE) { q->setGuidesConfig(rhs.guidesConfig); q->setMirrorAxisConfig(rhs.mirrorAxisConfig); q->setModified(rhs.modified); q->setAssistants(KisPaintingAssistant::cloneAssistantList(rhs.assistants)); q->setGridConfig(rhs.gridConfig); } else { // in CONSTRUCT mode, we cannot use the functions of KisDocument // because KisDocument does not yet have a pointer to us. guidesConfig = rhs.guidesConfig; mirrorAxisConfig = rhs.mirrorAxisConfig; modified = rhs.modified; assistants = KisPaintingAssistant::cloneAssistantList(rhs.assistants); gridConfig = rhs.gridConfig; } m_bAutoDetectedMime = rhs.m_bAutoDetectedMime; m_url = rhs.m_url; m_file = rhs.m_file; readwrite = rhs.readwrite; firstMod = rhs.firstMod; lastMod = rhs.lastMod; // XXX: the display properties will be shared between different snapshots globalAssistantsColor = rhs.globalAssistantsColor; if (policy == REPLACE) { QList newPaletteList = clonePaletteList(rhs.paletteList); q->setPaletteList(newPaletteList, /* emitSignal = */ true); // we still do not own palettes if we did not } else { paletteList = rhs.paletteList; } batchMode = rhs.batchMode; } QList KisDocument::Private::clonePaletteList(const QList &oldList) { QList newList; Q_FOREACH (KoColorSet *palette, oldList) { newList << new KoColorSet(*palette); } return newList; } class KisDocument::Private::StrippedSafeSavingLocker { public: StrippedSafeSavingLocker(QMutex *savingMutex, KisImageSP image) : m_locked(false) , m_image(image) , m_savingLock(savingMutex) , m_imageLock(image, true) { /** * Initial try to lock both objects. Locking the image guards * us from any image composition threads running in the * background, while savingMutex guards us from entering the * saving code twice by autosave and main threads. * * Since we are trying to lock multiple objects, so we should * do it in a safe manner. */ m_locked = std::try_lock(m_imageLock, m_savingLock) < 0; if (!m_locked) { m_image->requestStrokeEnd(); QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); // one more try... m_locked = std::try_lock(m_imageLock, m_savingLock) < 0; } } ~StrippedSafeSavingLocker() { if (m_locked) { m_imageLock.unlock(); m_savingLock.unlock(); } } bool successfullyLocked() const { return m_locked; } private: bool m_locked; KisImageSP m_image; StdLockableWrapper m_savingLock; KisImageBarrierLockAdapter m_imageLock; }; KisDocument::KisDocument() : d(new Private(this)) { connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); connect(d->undoStack, SIGNAL(cleanChanged(bool)), this, SLOT(slotUndoStackCleanChanged(bool))); connect(d->autoSaveTimer, SIGNAL(timeout()), this, SLOT(slotAutoSave())); setObjectName(newObjectName()); // preload the krita resources KisResourceServerProvider::instance(); d->shapeController = new KisShapeController(this, d->nserver); d->koShapeController = new KoShapeController(0, d->shapeController); d->shapeController->resourceManager()->setGlobalShapeController(d->koShapeController); slotConfigChanged(); } KisDocument::KisDocument(const KisDocument &rhs) : QObject(), d(new Private(*rhs.d, this)) { copyFromDocumentImpl(rhs, CONSTRUCT); } KisDocument::~KisDocument() { // wait until all the pending operations are in progress waitForSavingToComplete(); /** * Push a timebomb, which will try to release the memory after * the document has been deleted */ KisPaintDevice::createMemoryReleaseObject()->deleteLater(); d->autoSaveTimer->disconnect(this); d->autoSaveTimer->stop(); delete d->importExportManager; // Despite being QObject they needs to be deleted before the image delete d->shapeController; delete d->koShapeController; if (d->image) { d->image->notifyAboutToBeDeleted(); /** * WARNING: We should wait for all the internal image jobs to * finish before entering KisImage's destructor. The problem is, * while execution of KisImage::~KisImage, all the weak shared * pointers pointing to the image enter an inconsistent * state(!). The shared counter is already zero and destruction * has started, but the weak reference doesn't know about it, * because KisShared::~KisShared hasn't been executed yet. So all * the threads running in background and having weak pointers will * enter the KisImage's destructor as well. */ d->image->requestStrokeCancellation(); d->image->waitForDone(); // clear undo commands that can still point to the image d->undoStack->clear(); d->image->waitForDone(); KisImageWSP sanityCheckPointer = d->image; Q_UNUSED(sanityCheckPointer); // The following line trigger the deletion of the image d->image.clear(); // check if the image has actually been deleted KIS_SAFE_ASSERT_RECOVER_NOOP(!sanityCheckPointer.isValid()); } if (d->ownsPaletteList) { qDeleteAll(d->paletteList); } delete d; } bool KisDocument::reload() { // XXX: reimplement! return false; } KisDocument *KisDocument::clone() { return new KisDocument(*this); } bool KisDocument::exportDocumentImpl(const KritaUtils::ExportFileJob &job, KisPropertiesConfigurationSP exportConfiguration) { QFileInfo filePathInfo(job.filePath); if (filePathInfo.exists() && !filePathInfo.isWritable()) { slotCompleteSavingDocument(job, ImportExportCodes::NoAccessToWrite, i18n("%1 cannot be written to. Please save under a different name.", job.filePath)); //return ImportExportCodes::NoAccessToWrite; return false; } KisConfig cfg(true); if (cfg.backupFile() && filePathInfo.exists()) { QString backupDir; switch(cfg.readEntry("backupfilelocation", 0)) { case 1: backupDir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); break; case 2: backupDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); break; default: // Do nothing: the empty string is user file location break; } int numOfBackupsKept = cfg.readEntry("numberofbackupfiles", 1); QString suffix = cfg.readEntry("backupfilesuffix", "~"); if (numOfBackupsKept == 1) { if (!KBackup::simpleBackupFile(job.filePath, backupDir, suffix)) { qWarning() << "Failed to create simple backup file!" << job.filePath << backupDir << suffix; KisUsageLogger::log(QString("Failed to create a simple backup for %1 in %2.").arg(job.filePath).arg(backupDir.isEmpty() ? "the same location as the file" : backupDir)); return false; } else { KisUsageLogger::log(QString("Create a simple backup for %1 in %2.").arg(job.filePath).arg(backupDir.isEmpty() ? "the same location as the file" : backupDir)); } } else if (numOfBackupsKept > 2) { if (!KBackup::numberedBackupFile(job.filePath, backupDir, suffix, numOfBackupsKept)) { qWarning() << "Failed to create numbered backup file!" << job.filePath << backupDir << suffix; KisUsageLogger::log(QString("Failed to create a numbered backup for %2.").arg(job.filePath).arg(backupDir.isEmpty() ? "the same location as the file" : backupDir)); return false; } else { KisUsageLogger::log(QString("Create a simple backup for %1 in %2.").arg(job.filePath).arg(backupDir.isEmpty() ? "the same location as the file" : backupDir)); } } } //KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!job.mimeType.isEmpty(), false); if (job.mimeType.isEmpty()) { KisImportExportErrorCode error = ImportExportCodes::FileFormatIncorrect; slotCompleteSavingDocument(job, error, error.errorMessage()); return false; } const QString actionName = job.flags & KritaUtils::SaveIsExporting ? i18n("Exporting Document...") : i18n("Saving Document..."); bool started = initiateSavingInBackground(actionName, this, SLOT(slotCompleteSavingDocument(KritaUtils::ExportFileJob, KisImportExportErrorCode ,QString)), job, exportConfiguration); if (!started) { emit canceled(QString()); } return started; } bool KisDocument::exportDocument(const QUrl &url, const QByteArray &mimeType, bool showWarnings, KisPropertiesConfigurationSP exportConfiguration) { using namespace KritaUtils; SaveFlags flags = SaveIsExporting; if (showWarnings) { flags |= SaveShowWarnings; } KisUsageLogger::log(QString("Exporting Document: %1 as %2. %3 * %4 pixels, %5 layers, %6 frames, %7 framerate. Export configuration: %8") .arg(url.toLocalFile()) .arg(QString::fromLatin1(mimeType)) .arg(d->image->width()) .arg(d->image->height()) .arg(d->image->nlayers()) .arg(d->image->animationInterface()->totalLength()) .arg(d->image->animationInterface()->framerate()) .arg(exportConfiguration ? exportConfiguration->toXML() : "No configuration")); return exportDocumentImpl(KritaUtils::ExportFileJob(url.toLocalFile(), mimeType, flags), exportConfiguration); } bool KisDocument::saveAs(const QUrl &_url, const QByteArray &mimeType, bool showWarnings, KisPropertiesConfigurationSP exportConfiguration) { using namespace KritaUtils; KisUsageLogger::log(QString("Saving Document %9 as %1 (mime: %2). %3 * %4 pixels, %5 layers. %6 frames, %7 framerate. Export configuration: %8") .arg(_url.toLocalFile()) .arg(QString::fromLatin1(mimeType)) .arg(d->image->width()) .arg(d->image->height()) .arg(d->image->nlayers()) .arg(d->image->animationInterface()->totalLength()) .arg(d->image->animationInterface()->framerate()) .arg(exportConfiguration ? exportConfiguration->toXML() : "No configuration") .arg(url().toLocalFile())); return exportDocumentImpl(ExportFileJob(_url.toLocalFile(), mimeType, showWarnings ? SaveShowWarnings : SaveNone), exportConfiguration); } bool KisDocument::save(bool showWarnings, KisPropertiesConfigurationSP exportConfiguration) { return saveAs(url(), mimeType(), showWarnings, exportConfiguration); } QByteArray KisDocument::serializeToNativeByteArray() { QByteArray byteArray; QBuffer buffer(&byteArray); QScopedPointer filter(KisImportExportManager::filterForMimeType(nativeFormatMimeType(), KisImportExportManager::Export)); filter->setBatchMode(true); filter->setMimeType(nativeFormatMimeType()); Private::StrippedSafeSavingLocker locker(&d->savingMutex, d->image); if (!locker.successfullyLocked()) { return byteArray; } d->savingImage = d->image; if (!filter->convert(this, &buffer).isOk()) { qWarning() << "serializeToByteArray():: Could not export to our native format"; } return byteArray; } void KisDocument::slotCompleteSavingDocument(const KritaUtils::ExportFileJob &job, KisImportExportErrorCode status, const QString &errorMessage) { if (status.isCancelled()) return; const QString fileName = QFileInfo(job.filePath).fileName(); if (!status.isOk()) { emit statusBarMessage(i18nc("%1 --- failing file name, %2 --- error message", "Error during saving %1: %2", fileName, exportErrorToUserMessage(status, errorMessage)), errorMessageTimeout); if (!fileBatchMode()) { const QString filePath = job.filePath; QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("Could not save %1\nReason: %2", filePath, exportErrorToUserMessage(status, errorMessage))); } } else { if (!(job.flags & KritaUtils::SaveIsExporting)) { const QString existingAutoSaveBaseName = localFilePath(); const bool wasRecovered = isRecovered(); setUrl(QUrl::fromLocalFile(job.filePath)); setLocalFilePath(job.filePath); setMimeType(job.mimeType); updateEditingTime(true); if (!d->modifiedWhileSaving) { /** * If undo stack is already clean/empty, it doesn't emit any * signals, so we might forget update document modified state * (which was set, e.g. while recovering an autosave file) */ if (d->undoStack->isClean()) { setModified(false); } else { d->undoStack->setClean(); } } setRecovered(false); removeAutoSaveFiles(existingAutoSaveBaseName, wasRecovered); } emit completed(); emit sigSavingFinished(); emit statusBarMessage(i18n("Finished saving %1", fileName), successMessageTimeout); } } QByteArray KisDocument::mimeType() const { return d->mimeType; } void KisDocument::setMimeType(const QByteArray & mimeType) { d->mimeType = mimeType; } bool KisDocument::fileBatchMode() const { return d->batchMode; } void KisDocument::setFileBatchMode(const bool batchMode) { d->batchMode = batchMode; } KisDocument* KisDocument::lockAndCloneForSaving() { // force update of all the asynchronous nodes before cloning QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); KisLayerUtils::forceAllDelayedNodesUpdate(d->image->root()); KisMainWindow *window = KisPart::instance()->currentMainwindow(); if (window) { if (window->viewManager()) { if (!window->viewManager()->blockUntilOperationsFinished(d->image)) { return 0; } } } Private::StrippedSafeSavingLocker locker(&d->savingMutex, d->image); if (!locker.successfullyLocked()) { return 0; } return new KisDocument(*this); } KisDocument *KisDocument::lockAndCreateSnapshot() { KisDocument *doc = lockAndCloneForSaving(); if (doc) { // clone palette list doc->d->paletteList = doc->d->clonePaletteList(doc->d->paletteList); doc->d->ownsPaletteList = true; } return doc; } void KisDocument::copyFromDocument(const KisDocument &rhs) { copyFromDocumentImpl(rhs, REPLACE); } void KisDocument::copyFromDocumentImpl(const KisDocument &rhs, CopyPolicy policy) { if (policy == REPLACE) { d->copyFrom(*(rhs.d), this); d->undoStack->clear(); } else { // in CONSTRUCT mode, d should be already initialized connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); connect(d->undoStack, SIGNAL(cleanChanged(bool)), this, SLOT(slotUndoStackCleanChanged(bool))); connect(d->autoSaveTimer, SIGNAL(timeout()), this, SLOT(slotAutoSave())); d->shapeController = new KisShapeController(this, d->nserver); d->koShapeController = new KoShapeController(0, d->shapeController); d->shapeController->resourceManager()->setGlobalShapeController(d->koShapeController); } setObjectName(rhs.objectName()); slotConfigChanged(); if (rhs.d->image) { if (policy == REPLACE) { d->image->barrierLock(/* readOnly = */ false); rhs.d->image->barrierLock(/* readOnly = */ true); d->image->copyFromImage(*(rhs.d->image)); d->image->unlock(); rhs.d->image->unlock(); setCurrentImage(d->image, /* forceInitialUpdate = */ true); } else { // clone the image with keeping the GUIDs of the layers intact // NOTE: we expect the image to be locked! setCurrentImage(rhs.image()->clone(/* exactCopy = */ true), /* forceInitialUpdate = */ false); } } if (rhs.d->preActivatedNode) { QQueue linearizedNodes; KisLayerUtils::recursiveApplyNodes(rhs.d->image->root(), [&linearizedNodes](KisNodeSP node) { linearizedNodes.enqueue(node); }); KisLayerUtils::recursiveApplyNodes(d->image->root(), [&linearizedNodes, &rhs, this](KisNodeSP node) { KisNodeSP refNode = linearizedNodes.dequeue(); if (rhs.d->preActivatedNode.data() == refNode.data()) { d->preActivatedNode = node; } }); } // reinitialize references' signal connection KisReferenceImagesLayerSP referencesLayer = this->referenceImagesLayer(); setReferenceImagesLayer(referencesLayer, false); KisDecorationsWrapperLayerSP decorationsLayer = KisLayerUtils::findNodeByType(d->image->root()); if (decorationsLayer) { decorationsLayer->setDocument(this); } if (policy == REPLACE) { setModified(true); } } bool KisDocument::exportDocumentSync(const QUrl &url, const QByteArray &mimeType, KisPropertiesConfigurationSP exportConfiguration) { { /** - * The caller guarantees that noone else uses the document (usually, - * it is a temporary docuent created specifically for exporting), so + * The caller guarantees that no one else uses the document (usually, + * it is a temporary document created specifically for exporting), so * we don't need to copy or lock the document. Instead we should just * ensure the barrier lock is synced and then released. */ Private::StrippedSafeSavingLocker locker(&d->savingMutex, d->image); if (!locker.successfullyLocked()) { return false; } } d->savingImage = d->image; const QString fileName = url.toLocalFile(); KisImportExportErrorCode status = d->importExportManager-> exportDocument(fileName, fileName, mimeType, false, exportConfiguration); d->savingImage = 0; return status.isOk(); } bool KisDocument::initiateSavingInBackground(const QString actionName, const QObject *receiverObject, const char *receiverMethod, const KritaUtils::ExportFileJob &job, KisPropertiesConfigurationSP exportConfiguration) { return initiateSavingInBackground(actionName, receiverObject, receiverMethod, job, exportConfiguration, std::unique_ptr()); } bool KisDocument::initiateSavingInBackground(const QString actionName, const QObject *receiverObject, const char *receiverMethod, const KritaUtils::ExportFileJob &job, KisPropertiesConfigurationSP exportConfiguration, std::unique_ptr &&optionalClonedDocument) { KIS_ASSERT_RECOVER_RETURN_VALUE(job.isValid(), false); QScopedPointer clonedDocument; if (!optionalClonedDocument) { clonedDocument.reset(lockAndCloneForSaving()); } else { clonedDocument.reset(optionalClonedDocument.release()); } // we block saving until the current saving is finished! if (!clonedDocument || !d->savingMutex.tryLock()) { return false; } auto waitForImage = [] (KisImageSP image) { KisMainWindow *window = KisPart::instance()->currentMainwindow(); if (window) { if (window->viewManager()) { window->viewManager()->blockUntilOperationsFinishedForced(image); } } }; { KisNodeSP newRoot = clonedDocument->image()->root(); KIS_SAFE_ASSERT_RECOVER(!KisLayerUtils::hasDelayedNodeWithUpdates(newRoot)) { KisLayerUtils::forceAllDelayedNodesUpdate(newRoot); waitForImage(clonedDocument->image()); } } KIS_SAFE_ASSERT_RECOVER(clonedDocument->image()->isIdle()) { waitForImage(clonedDocument->image()); } KIS_ASSERT_RECOVER_RETURN_VALUE(!d->backgroundSaveDocument, false); KIS_ASSERT_RECOVER_RETURN_VALUE(!d->backgroundSaveJob.isValid(), false); d->backgroundSaveDocument.reset(clonedDocument.take()); d->backgroundSaveJob = job; d->modifiedWhileSaving = false; if (d->backgroundSaveJob.flags & KritaUtils::SaveInAutosaveMode) { d->backgroundSaveDocument->d->isAutosaving = true; } connect(d->backgroundSaveDocument.data(), SIGNAL(sigBackgroundSavingFinished(KisImportExportErrorCode, QString)), this, SLOT(slotChildCompletedSavingInBackground(KisImportExportErrorCode, QString))); connect(this, SIGNAL(sigCompleteBackgroundSaving(KritaUtils::ExportFileJob, KisImportExportErrorCode, QString)), receiverObject, receiverMethod, Qt::UniqueConnection); bool started = d->backgroundSaveDocument->startExportInBackground(actionName, job.filePath, job.filePath, job.mimeType, job.flags & KritaUtils::SaveShowWarnings, exportConfiguration); if (!started) { // the state should have been deinitialized in slotChildCompletedSavingInBackground() KIS_SAFE_ASSERT_RECOVER (!d->backgroundSaveDocument && !d->backgroundSaveJob.isValid()) { d->backgroundSaveDocument.take()->deleteLater(); d->savingMutex.unlock(); d->backgroundSaveJob = KritaUtils::ExportFileJob(); } } return started; } void KisDocument::slotChildCompletedSavingInBackground(KisImportExportErrorCode status, const QString &errorMessage) { KIS_ASSERT_RECOVER_RETURN(isSaving()); KIS_ASSERT_RECOVER(d->backgroundSaveDocument) { d->savingMutex.unlock(); return; } if (d->backgroundSaveJob.flags & KritaUtils::SaveInAutosaveMode) { d->backgroundSaveDocument->d->isAutosaving = false; } d->backgroundSaveDocument.take()->deleteLater(); KIS_ASSERT_RECOVER(d->backgroundSaveJob.isValid()) { d->savingMutex.unlock(); return; } const KritaUtils::ExportFileJob job = d->backgroundSaveJob; d->backgroundSaveJob = KritaUtils::ExportFileJob(); // unlock at the very end d->savingMutex.unlock(); QFileInfo fi(job.filePath); KisUsageLogger::log(QString("Completed saving %1 (mime: %2). Result: %3. Size: %4. MD5 Hash: %5") .arg(job.filePath) .arg(QString::fromLatin1(job.mimeType)) .arg(!status.isOk() ? exportErrorToUserMessage(status, errorMessage) : "OK") .arg(fi.size()) .arg(QString::fromLatin1(KoMD5Generator().generateHash(job.filePath).toHex()))); emit sigCompleteBackgroundSaving(job, status, errorMessage); } void KisDocument::slotAutoSaveImpl(std::unique_ptr &&optionalClonedDocument) { if (!d->modified || !d->modifiedAfterAutosave) return; const QString autoSaveFileName = generateAutoSaveFileName(localFilePath()); emit statusBarMessage(i18n("Autosaving... %1", autoSaveFileName), successMessageTimeout); KisUsageLogger::log(QString("Autosaving: %1").arg(autoSaveFileName)); const bool hadClonedDocument = bool(optionalClonedDocument); bool started = false; if (d->image->isIdle() || hadClonedDocument) { started = initiateSavingInBackground(i18n("Autosaving..."), this, SLOT(slotCompleteAutoSaving(KritaUtils::ExportFileJob, KisImportExportErrorCode, QString)), KritaUtils::ExportFileJob(autoSaveFileName, nativeFormatMimeType(), KritaUtils::SaveIsExporting | KritaUtils::SaveInAutosaveMode), 0, std::move(optionalClonedDocument)); } else { emit statusBarMessage(i18n("Autosaving postponed: document is busy..."), errorMessageTimeout); } if (!started && !hadClonedDocument && d->autoSaveFailureCount >= 3) { KisCloneDocumentStroke *stroke = new KisCloneDocumentStroke(this); connect(stroke, SIGNAL(sigDocumentCloned(KisDocument*)), this, SLOT(slotInitiateAsyncAutosaving(KisDocument*)), Qt::BlockingQueuedConnection); KisStrokeId strokeId = d->image->startStroke(stroke); d->image->endStroke(strokeId); setInfiniteAutoSaveInterval(); } else if (!started) { setEmergencyAutoSaveInterval(); } else { d->modifiedAfterAutosave = false; } } void KisDocument::slotAutoSave() { slotAutoSaveImpl(std::unique_ptr()); } void KisDocument::slotInitiateAsyncAutosaving(KisDocument *clonedDocument) { slotAutoSaveImpl(std::unique_ptr(clonedDocument)); } void KisDocument::slotCompleteAutoSaving(const KritaUtils::ExportFileJob &job, KisImportExportErrorCode status, const QString &errorMessage) { Q_UNUSED(job); const QString fileName = QFileInfo(job.filePath).fileName(); if (!status.isOk()) { setEmergencyAutoSaveInterval(); emit statusBarMessage(i18nc("%1 --- failing file name, %2 --- error message", "Error during autosaving %1: %2", fileName, exportErrorToUserMessage(status, errorMessage)), errorMessageTimeout); } else { KisConfig cfg(true); d->autoSaveDelay = cfg.autoSaveInterval(); if (!d->modifiedWhileSaving) { d->autoSaveTimer->stop(); // until the next change d->autoSaveFailureCount = 0; } else { setNormalAutoSaveInterval(); } emit statusBarMessage(i18n("Finished autosaving %1", fileName), successMessageTimeout); } } bool KisDocument::startExportInBackground(const QString &actionName, const QString &location, const QString &realLocation, const QByteArray &mimeType, bool showWarnings, KisPropertiesConfigurationSP exportConfiguration) { d->savingImage = d->image; KisMainWindow *window = KisPart::instance()->currentMainwindow(); if (window) { if (window->viewManager()) { d->savingUpdater = window->viewManager()->createThreadedUpdater(actionName); d->importExportManager->setUpdater(d->savingUpdater); } } KisImportExportErrorCode initializationStatus(ImportExportCodes::OK); d->childSavingFuture = d->importExportManager->exportDocumentAsyc(location, realLocation, mimeType, initializationStatus, showWarnings, exportConfiguration); if (!initializationStatus.isOk()) { if (d->savingUpdater) { d->savingUpdater->cancel(); } d->savingImage.clear(); emit sigBackgroundSavingFinished(initializationStatus, initializationStatus.errorMessage()); return false; } typedef QFutureWatcher StatusWatcher; StatusWatcher *watcher = new StatusWatcher(); watcher->setFuture(d->childSavingFuture); connect(watcher, SIGNAL(finished()), SLOT(finishExportInBackground())); connect(watcher, SIGNAL(finished()), watcher, SLOT(deleteLater())); return true; } void KisDocument::finishExportInBackground() { KIS_SAFE_ASSERT_RECOVER(d->childSavingFuture.isFinished()) { emit sigBackgroundSavingFinished(ImportExportCodes::InternalError, ""); return; } KisImportExportErrorCode status = d->childSavingFuture.result(); const QString errorMessage = status.errorMessage(); d->savingImage.clear(); d->childSavingFuture = QFuture(); d->lastErrorMessage.clear(); if (d->savingUpdater) { d->savingUpdater->setProgress(100); } emit sigBackgroundSavingFinished(status, errorMessage); } void KisDocument::setReadWrite(bool readwrite) { d->readwrite = readwrite; setNormalAutoSaveInterval(); Q_FOREACH (KisMainWindow *mainWindow, KisPart::instance()->mainWindows()) { mainWindow->setReadWrite(readwrite); } } void KisDocument::setAutoSaveDelay(int delay) { if (isReadWrite() && delay > 0) { d->autoSaveTimer->start(delay * 1000); } else { d->autoSaveTimer->stop(); } } void KisDocument::setNormalAutoSaveInterval() { setAutoSaveDelay(d->autoSaveDelay); d->autoSaveFailureCount = 0; } void KisDocument::setEmergencyAutoSaveInterval() { const int emergencyAutoSaveInterval = 10; /* sec */ setAutoSaveDelay(emergencyAutoSaveInterval); d->autoSaveFailureCount++; } void KisDocument::setInfiniteAutoSaveInterval() { setAutoSaveDelay(-1); } KoDocumentInfo *KisDocument::documentInfo() const { return d->docInfo; } bool KisDocument::isModified() const { return d->modified; } QPixmap KisDocument::generatePreview(const QSize& size) { KisImageSP image = d->image; if (d->savingImage) image = d->savingImage; if (image) { QRect bounds = image->bounds(); QSize newSize = bounds.size(); newSize.scale(size, Qt::KeepAspectRatio); QPixmap px = QPixmap::fromImage(image->convertToQImage(newSize, 0)); if (px.size() == QSize(0,0)) { px = QPixmap(newSize); QPainter gc(&px); QBrush checkBrush = QBrush(KisCanvasWidgetBase::createCheckersImage(newSize.width() / 5)); gc.fillRect(px.rect(), checkBrush); gc.end(); } return px; } return QPixmap(size); } QString KisDocument::generateAutoSaveFileName(const QString & path) const { QString retval; // Using the extension allows to avoid relying on the mime magic when opening const QString extension (".kra"); QString prefix = KisConfig(true).readEntry("autosavefileshidden") ? QString(".") : QString(); QRegularExpression autosavePattern1("^\\..+-autosave.kra$"); QRegularExpression autosavePattern2("^.+-autosave.kra$"); QFileInfo fi(path); QString dir = fi.absolutePath(); QString filename = fi.fileName(); if (path.isEmpty() || autosavePattern1.match(filename).hasMatch() || autosavePattern2.match(filename).hasMatch() || !fi.isWritable()) { // Never saved? #ifdef Q_OS_WIN // On Windows, use the temp location (https://bugs.kde.org/show_bug.cgi?id=314921) retval = QString("%1%2%7%3-%4-%5-autosave%6").arg(QDir::tempPath()).arg(QDir::separator()).arg("krita").arg(qApp->applicationPid()).arg(objectName()).arg(extension).arg(prefix); #else // On Linux, use a temp file in $HOME then. Mark it with the pid so two instances don't overwrite each other's autosave file retval = QString("%1%2%7%3-%4-%5-autosave%6").arg(QDir::homePath()).arg(QDir::separator()).arg("krita").arg(qApp->applicationPid()).arg(objectName()).arg(extension).arg(prefix); #endif } else { retval = QString("%1%2%5%3-autosave%4").arg(dir).arg(QDir::separator()).arg(filename).arg(extension).arg(prefix); } //qDebug() << "generateAutoSaveFileName() for path" << path << ":" << retval; return retval; } bool KisDocument::importDocument(const QUrl &_url) { bool ret; dbgUI << "url=" << _url.url(); // open... ret = openUrl(_url); // reset url & m_file (kindly? set by KisParts::openUrl()) to simulate a // File --> Import if (ret) { dbgUI << "success, resetting url"; resetURL(); setTitleModified(); } return ret; } bool KisDocument::openUrl(const QUrl &_url, OpenFlags flags) { if (!_url.isLocalFile()) { return false; } dbgUI << "url=" << _url.url(); d->lastErrorMessage.clear(); // Reimplemented, to add a check for autosave files and to improve error reporting if (!_url.isValid()) { d->lastErrorMessage = i18n("Malformed URL\n%1", _url.url()); // ## used anywhere ? return false; } QUrl url(_url); bool autosaveOpened = false; if (url.isLocalFile() && !fileBatchMode()) { QString file = url.toLocalFile(); QString asf = generateAutoSaveFileName(file); if (QFile::exists(asf)) { KisApplication *kisApp = static_cast(qApp); kisApp->hideSplashScreen(); //dbgUI <<"asf=" << asf; // ## TODO compare timestamps ? int res = QMessageBox::warning(0, i18nc("@title:window", "Krita"), i18n("An autosaved file exists for this document.\nDo you want to open the autosaved file instead?"), QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::Yes); switch (res) { case QMessageBox::Yes : url.setPath(asf); autosaveOpened = true; break; case QMessageBox::No : QFile::remove(asf); break; default: // Cancel return false; } } } bool ret = openUrlInternal(url); if (autosaveOpened || flags & RecoveryFile) { setReadWrite(true); // enable save button setModified(true); setRecovered(true); } else { if (ret) { if (!(flags & DontAddToRecent)) { KisPart::instance()->addRecentURLToAllMainWindows(_url); } // Detect readonly local-files; remote files are assumed to be writable QFileInfo fi(url.toLocalFile()); setReadWrite(fi.isWritable()); } setRecovered(false); } return ret; } class DlgLoadMessages : public KoDialog { public: DlgLoadMessages(const QString &title, const QString &message, const QStringList &warnings) { setWindowTitle(title); setWindowIcon(KisIconUtils::loadIcon("warning")); QWidget *page = new QWidget(this); QVBoxLayout *layout = new QVBoxLayout(page); QHBoxLayout *hlayout = new QHBoxLayout(); QLabel *labelWarning= new QLabel(); labelWarning->setPixmap(KisIconUtils::loadIcon("warning").pixmap(32, 32)); hlayout->addWidget(labelWarning); hlayout->addWidget(new QLabel(message)); layout->addLayout(hlayout); QTextBrowser *browser = new QTextBrowser(); QString warning = "

"; if (warnings.size() == 1) { warning += " Reason:

"; } else { warning += " Reasons:

"; } warning += "

    "; Q_FOREACH(const QString &w, warnings) { warning += "\n
  • " + w + "
  • "; } warning += "
"; browser->setHtml(warning); browser->setMinimumHeight(200); browser->setMinimumWidth(400); layout->addWidget(browser); setMainWidget(page); setButtons(KoDialog::Ok); resize(minimumSize()); } }; bool KisDocument::openFile() { //dbgUI <<"for" << localFilePath(); if (!QFile::exists(localFilePath()) && !fileBatchMode()) { QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("File %1 does not exist.", localFilePath())); return false; } QString filename = localFilePath(); QString typeName = mimeType(); if (typeName.isEmpty()) { typeName = KisMimeDatabase::mimeTypeForFile(filename); } //qDebug() << "mimetypes 4:" << typeName; // Allow to open backup files, don't keep the mimetype application/x-trash. if (typeName == "application/x-trash") { QString path = filename; while (path.length() > 0) { path.chop(1); typeName = KisMimeDatabase::mimeTypeForFile(path); //qDebug() << "\t" << path << typeName; if (!typeName.isEmpty()) { break; } } //qDebug() << "chopped" << filename << "to" << path << "Was trash, is" << typeName; } dbgUI << localFilePath() << "type:" << typeName; KisMainWindow *window = KisPart::instance()->currentMainwindow(); KoUpdaterPtr updater; if (window && window->viewManager()) { updater = window->viewManager()->createUnthreadedUpdater(i18n("Opening document")); d->importExportManager->setUpdater(updater); } KisImportExportErrorCode status = d->importExportManager->importDocument(localFilePath(), typeName); if (!status.isOk()) { if (window && window->viewManager()) { updater->cancel(); } QString msg = status.errorMessage(); if (!msg.isEmpty() && !fileBatchMode()) { DlgLoadMessages dlg(i18nc("@title:window", "Krita"), i18n("Could not open %2.\nReason: %1.", msg, prettyPathOrUrl()), errorMessage().split("\n") + warningMessage().split("\n")); dlg.exec(); } return false; } else if (!warningMessage().isEmpty() && !fileBatchMode()) { DlgLoadMessages dlg(i18nc("@title:window", "Krita"), i18n("There were problems opening %1.", prettyPathOrUrl()), warningMessage().split("\n")); dlg.exec(); setUrl(QUrl()); } setMimeTypeAfterLoading(typeName); d->syncDecorationsWrapperLayerState(); emit sigLoadingFinished(); undoStack()->clear(); return true; } void KisDocument::autoSaveOnPause() { if (!d->modified || !d->modifiedAfterAutosave) return; const QString autoSaveFileName = generateAutoSaveFileName(localFilePath()); QUrl url("file:/" + autoSaveFileName); bool started = exportDocumentSync(url, nativeFormatMimeType()); if (started) { d->modifiedAfterAutosave = false; dbgAndroid << "autoSaveOnPause successful"; } else { qWarning() << "Could not auto-save when paused"; } } // shared between openFile and koMainWindow's "create new empty document" code void KisDocument::setMimeTypeAfterLoading(const QString& mimeType) { d->mimeType = mimeType.toLatin1(); d->outputMimeType = d->mimeType; } bool KisDocument::loadNativeFormat(const QString & file_) { return openUrl(QUrl::fromLocalFile(file_)); } void KisDocument::setModified(bool mod) { if (mod) { updateEditingTime(false); } if (d->isAutosaving) // ignore setModified calls due to autosaving return; if ( !d->readwrite && d->modified ) { errKrita << "Can't set a read-only document to 'modified' !" << endl; return; } //dbgUI<<" url:" << url.path(); //dbgUI<<" mod="<docInfo->aboutInfo("editing-time").toInt() + d->firstMod.secsTo(d->lastMod))); d->firstMod = now; } else if (firstModDelta > 60 || forceStoreElapsed) { d->docInfo->setAboutInfo("editing-time", QString::number(d->docInfo->aboutInfo("editing-time").toInt() + firstModDelta)); d->firstMod = now; } d->lastMod = now; } QString KisDocument::prettyPathOrUrl() const { QString _url(url().toDisplayString()); #ifdef Q_OS_WIN if (url().isLocalFile()) { _url = QDir::toNativeSeparators(_url); } #endif return _url; } // Get caption from document info (title(), in about page) QString KisDocument::caption() const { QString c; const QString _url(url().fileName()); // if URL is empty...it is probably an unsaved file if (_url.isEmpty()) { c = " [" + i18n("Not Saved") + "] "; } else { c = _url; // Fall back to document URL } return c; } void KisDocument::setTitleModified() { emit titleModified(caption(), isModified()); } QDomDocument KisDocument::createDomDocument(const QString& tagName, const QString& version) const { return createDomDocument("krita", tagName, version); } //static QDomDocument KisDocument::createDomDocument(const QString& appName, const QString& tagName, const QString& version) { QDomImplementation impl; QString url = QString("http://www.calligra.org/DTD/%1-%2.dtd").arg(appName).arg(version); QDomDocumentType dtype = impl.createDocumentType(tagName, QString("-//KDE//DTD %1 %2//EN").arg(appName).arg(version), url); // The namespace URN doesn't need to include the version number. QString namespaceURN = QString("http://www.calligra.org/DTD/%1").arg(appName); QDomDocument doc = impl.createDocument(namespaceURN, tagName, dtype); doc.insertBefore(doc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\""), doc.documentElement()); return doc; } bool KisDocument::isNativeFormat(const QByteArray& mimetype) const { if (mimetype == nativeFormatMimeType()) return true; return extraNativeMimeTypes().contains(mimetype); } void KisDocument::setErrorMessage(const QString& errMsg) { d->lastErrorMessage = errMsg; } QString KisDocument::errorMessage() const { return d->lastErrorMessage; } void KisDocument::setWarningMessage(const QString& warningMsg) { d->lastWarningMessage = warningMsg; } QString KisDocument::warningMessage() const { return d->lastWarningMessage; } void KisDocument::removeAutoSaveFiles(const QString &autosaveBaseName, bool wasRecovered) { //qDebug() << "removeAutoSaveFiles"; // Eliminate any auto-save file QString asf = generateAutoSaveFileName(autosaveBaseName); // the one in the current dir //qDebug() << "\tfilename:" << asf << "exists:" << QFile::exists(asf); if (QFile::exists(asf)) { //qDebug() << "\tremoving autosavefile" << asf; QFile::remove(asf); } asf = generateAutoSaveFileName(QString()); // and the one in $HOME //qDebug() << "Autsavefile in $home" << asf; if (QFile::exists(asf)) { //qDebug() << "\tremoving autsavefile 2" << asf; QFile::remove(asf); } QList expressions; expressions << QRegularExpression("^\\..+-autosave.kra$") << QRegularExpression("^.+-autosave.kra$"); Q_FOREACH(const QRegularExpression &rex, expressions) { if (wasRecovered && !autosaveBaseName.isEmpty() && rex.match(QFileInfo(autosaveBaseName).fileName()).hasMatch() && QFile::exists(autosaveBaseName)) { QFile::remove(autosaveBaseName); } } } KoUnit KisDocument::unit() const { return d->unit; } void KisDocument::setUnit(const KoUnit &unit) { if (d->unit != unit) { d->unit = unit; emit unitChanged(unit); } } KUndo2Stack *KisDocument::undoStack() { return d->undoStack; } KisImportExportManager *KisDocument::importExportManager() const { return d->importExportManager; } void KisDocument::addCommand(KUndo2Command *command) { if (command) d->undoStack->push(command); } void KisDocument::beginMacro(const KUndo2MagicString & text) { d->undoStack->beginMacro(text); } void KisDocument::endMacro() { d->undoStack->endMacro(); } void KisDocument::slotUndoStackCleanChanged(bool value) { setModified(!value); } void KisDocument::slotConfigChanged() { KisConfig cfg(true); if (d->undoStack->undoLimit() != cfg.undoStackLimit()) { if (!d->undoStack->isClean()) { d->undoStack->clear(); } d->undoStack->setUndoLimit(cfg.undoStackLimit()); } d->autoSaveDelay = cfg.autoSaveInterval(); setNormalAutoSaveInterval(); } void KisDocument::slotImageRootChanged() { d->syncDecorationsWrapperLayerState(); } void KisDocument::clearUndoHistory() { d->undoStack->clear(); } KisGridConfig KisDocument::gridConfig() const { return d->gridConfig; } void KisDocument::setGridConfig(const KisGridConfig &config) { if (d->gridConfig != config) { d->gridConfig = config; d->syncDecorationsWrapperLayerState(); emit sigGridConfigChanged(config); } } QList &KisDocument::paletteList() { return d->paletteList; } void KisDocument::setPaletteList(const QList &paletteList, bool emitSignal) { if (d->paletteList != paletteList) { QList oldPaletteList = d->paletteList; d->paletteList = paletteList; if (emitSignal) { emit sigPaletteListChanged(oldPaletteList, paletteList); } } } const KisGuidesConfig& KisDocument::guidesConfig() const { return d->guidesConfig; } void KisDocument::setGuidesConfig(const KisGuidesConfig &data) { if (d->guidesConfig == data) return; d->guidesConfig = data; d->syncDecorationsWrapperLayerState(); emit sigGuidesConfigChanged(d->guidesConfig); } const KisMirrorAxisConfig& KisDocument::mirrorAxisConfig() const { return d->mirrorAxisConfig; } void KisDocument::setMirrorAxisConfig(const KisMirrorAxisConfig &config) { if (d->mirrorAxisConfig == config) { return; } d->mirrorAxisConfig = config; setModified(true); emit sigMirrorAxisConfigChanged(); } void KisDocument::resetURL() { setUrl(QUrl()); setLocalFilePath(QString()); } KoDocumentInfoDlg *KisDocument::createDocumentInfoDialog(QWidget *parent, KoDocumentInfo *docInfo) const { return new KoDocumentInfoDlg(parent, docInfo); } bool KisDocument::isReadWrite() const { return d->readwrite; } QUrl KisDocument::url() const { return d->m_url; } bool KisDocument::closeUrl(bool promptToSave) { if (promptToSave) { if ( isReadWrite() && isModified()) { Q_FOREACH (KisView *view, KisPart::instance()->views()) { if (view && view->document() == this) { if (!view->queryClose()) { return false; } } } } } // Not modified => ok and delete temp file. d->mimeType = QByteArray(); // It always succeeds for a read-only part, // but the return value exists for reimplementations // (e.g. pressing cancel for a modified read-write part) return true; } void KisDocument::setUrl(const QUrl &url) { d->m_url = url; } QString KisDocument::localFilePath() const { return d->m_file; } void KisDocument::setLocalFilePath( const QString &localFilePath ) { d->m_file = localFilePath; } bool KisDocument::openUrlInternal(const QUrl &url) { if ( !url.isValid() ) { return false; } if (d->m_bAutoDetectedMime) { d->mimeType = QByteArray(); d->m_bAutoDetectedMime = false; } QByteArray mimetype = d->mimeType; if ( !closeUrl() ) { return false; } d->mimeType = mimetype; setUrl(url); d->m_file.clear(); if (d->m_url.isLocalFile()) { d->m_file = d->m_url.toLocalFile(); bool ret; // set the mimetype only if it was not already set (for example, by the host application) if (d->mimeType.isEmpty()) { // get the mimetype of the file // using findByUrl() to avoid another string -> url conversion QString mime = KisMimeDatabase::mimeTypeForFile(d->m_url.toLocalFile()); d->mimeType = mime.toLocal8Bit(); d->m_bAutoDetectedMime = true; } setUrl(d->m_url); ret = openFile(); if (ret) { emit completed(); } else { emit canceled(QString()); } return ret; } return false; } bool KisDocument::newImage(const QString& name, qint32 width, qint32 height, const KoColorSpace* cs, const KoColor &bgColor, KisConfig::BackgroundStyle bgStyle, int numberOfLayers, const QString &description, const double imageResolution) { Q_ASSERT(cs); KisImageSP image; if (!cs) return false; QApplication::setOverrideCursor(Qt::BusyCursor); image = new KisImage(createUndoStore(), width, height, cs, name); Q_CHECK_PTR(image); connect(image, SIGNAL(sigImageModified()), this, SLOT(setImageModified()), Qt::UniqueConnection); image->setResolution(imageResolution, imageResolution); image->assignImageProfile(cs->profile()); documentInfo()->setAboutInfo("title", name); documentInfo()->setAboutInfo("abstract", description); KisLayerSP layer; if (bgStyle == KisConfig::RASTER_LAYER || bgStyle == KisConfig::FILL_LAYER) { KoColor strippedAlpha = bgColor; strippedAlpha.setOpacity(OPACITY_OPAQUE_U8); if (bgStyle == KisConfig::RASTER_LAYER) { layer = new KisPaintLayer(image.data(), "Background", OPACITY_OPAQUE_U8, cs);; layer->paintDevice()->setDefaultPixel(strippedAlpha); } else if (bgStyle == KisConfig::FILL_LAYER) { KisFilterConfigurationSP filter_config = KisGeneratorRegistry::instance()->get("color")->defaultConfiguration(); filter_config->setProperty("color", strippedAlpha.toQColor()); layer = new KisGeneratorLayer(image.data(), "Background Fill", filter_config, image->globalSelection()); } layer->setOpacity(bgColor.opacityU8()); if (numberOfLayers > 1) { //Lock bg layer if others are present. layer->setUserLocked(true); } } else { // KisConfig::CANVAS_COLOR (needs an unlocked starting layer). image->setDefaultProjectionColor(bgColor); layer = new KisPaintLayer(image.data(), image->nextLayerName(), OPACITY_OPAQUE_U8, cs); } Q_CHECK_PTR(layer); image->addNode(layer.data(), image->rootLayer().data()); layer->setDirty(QRect(0, 0, width, height)); setCurrentImage(image); for(int i = 1; i < numberOfLayers; ++i) { KisPaintLayerSP layer = new KisPaintLayer(image, image->nextLayerName(), OPACITY_OPAQUE_U8, cs); image->addNode(layer, image->root(), i); layer->setDirty(QRect(0, 0, width, height)); } KisConfig cfg(false); cfg.defImageWidth(width); cfg.defImageHeight(height); cfg.defImageResolution(imageResolution); cfg.defColorModel(image->colorSpace()->colorModelId().id()); cfg.setDefaultColorDepth(image->colorSpace()->colorDepthId().id()); cfg.defColorProfile(image->colorSpace()->profile()->name()); KisUsageLogger::log(i18n("Created image \"%1\", %2 * %3 pixels, %4 dpi. Color model: %6 %5 (%7). Layers: %8" , name , width, height , imageResolution * 72.0 , image->colorSpace()->colorModelId().name(), image->colorSpace()->colorDepthId().name() , image->colorSpace()->profile()->name() , numberOfLayers)); QApplication::restoreOverrideCursor(); return true; } bool KisDocument::isSaving() const { const bool result = d->savingMutex.tryLock(); if (result) { d->savingMutex.unlock(); } return !result; } void KisDocument::waitForSavingToComplete() { if (isSaving()) { KisAsyncActionFeedback f(i18nc("progress dialog message when the user closes the document that is being saved", "Waiting for saving to complete..."), 0); f.waitForMutex(&d->savingMutex); } } KoShapeControllerBase *KisDocument::shapeController() const { return d->shapeController; } KoShapeLayer* KisDocument::shapeForNode(KisNodeSP layer) const { return d->shapeController->shapeForNode(layer); } QList KisDocument::assistants() const { return d->assistants; } void KisDocument::setAssistants(const QList &value) { if (d->assistants != value) { d->assistants = value; d->syncDecorationsWrapperLayerState(); emit sigAssistantsChanged(); } } KisReferenceImagesLayerSP KisDocument::referenceImagesLayer() const { if (!d->image) return KisReferenceImagesLayerSP(); KisReferenceImagesLayerSP referencesLayer = KisLayerUtils::findNodeByType(d->image->root()); return referencesLayer; } void KisDocument::setReferenceImagesLayer(KisSharedPtr layer, bool updateImage) { KisReferenceImagesLayerSP currentReferenceLayer = referenceImagesLayer(); if (currentReferenceLayer == layer) { return; } if (currentReferenceLayer) { currentReferenceLayer->disconnect(this); } if (updateImage) { if (currentReferenceLayer) { d->image->removeNode(currentReferenceLayer); } if (layer) { d->image->addNode(layer); } } currentReferenceLayer = layer; if (currentReferenceLayer) { connect(currentReferenceLayer, SIGNAL(sigUpdateCanvas(QRectF)), this, SIGNAL(sigReferenceImagesChanged())); } emit sigReferenceImagesLayerChanged(layer); } void KisDocument::setPreActivatedNode(KisNodeSP activatedNode) { d->preActivatedNode = activatedNode; } KisNodeSP KisDocument::preActivatedNode() const { return d->preActivatedNode; } KisImageWSP KisDocument::image() const { return d->image; } KisImageSP KisDocument::savingImage() const { return d->savingImage; } void KisDocument::setCurrentImage(KisImageSP image, bool forceInitialUpdate) { if (d->image) { // Disconnect existing sig/slot connections d->image->setUndoStore(new KisDumbUndoStore()); d->image->disconnect(this); d->shapeController->setImage(0); d->image = 0; } if (!image) return; d->setImageAndInitIdleWatcher(image); d->image->setUndoStore(new KisDocumentUndoStore(this)); d->shapeController->setImage(image); setModified(false); connect(d->image, SIGNAL(sigImageModified()), this, SLOT(setImageModified()), Qt::UniqueConnection); connect(d->image, SIGNAL(sigLayersChangedAsync()), this, SLOT(slotImageRootChanged())); if (forceInitialUpdate) { d->image->initialRefreshGraph(); } } void KisDocument::hackPreliminarySetImage(KisImageSP image) { KIS_SAFE_ASSERT_RECOVER_RETURN(!d->image); d->setImageAndInitIdleWatcher(image); d->shapeController->setImage(image); } void KisDocument::setImageModified() { // we only set as modified if undo stack is not at clean state setModified(!d->undoStack->isClean()); } KisUndoStore* KisDocument::createUndoStore() { return new KisDocumentUndoStore(this); } bool KisDocument::isAutosaving() const { return d->isAutosaving; } QString KisDocument::exportErrorToUserMessage(KisImportExportErrorCode status, const QString &errorMessage) { return errorMessage.isEmpty() ? status.errorMessage() : errorMessage; } void KisDocument::setAssistantsGlobalColor(QColor color) { d->globalAssistantsColor = color; } QColor KisDocument::assistantsGlobalColor() { return d->globalAssistantsColor; } QRectF KisDocument::documentBounds() const { QRectF bounds = d->image->bounds(); KisReferenceImagesLayerSP referenceImagesLayer = this->referenceImagesLayer(); if (referenceImagesLayer) { bounds |= referenceImagesLayer->boundingImageRect(); } return bounds; } diff --git a/libs/ui/KisImportExportManager.cpp b/libs/ui/KisImportExportManager.cpp index daa749ef19..88b62a3c9e 100644 --- a/libs/ui/KisImportExportManager.cpp +++ b/libs/ui/KisImportExportManager.cpp @@ -1,725 +1,725 @@ /* * Copyright (C) 2016 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 "KisImportExportManager.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_config.h" #include "KisImportExportFilter.h" #include "KisDocument.h" #include #include #include "kis_painter.h" #include "kis_guides_config.h" #include "kis_grid_config.h" #include "kis_popup_button.h" #include #include "kis_async_action_feedback.h" #include "KisReferenceImagesLayer.h" // static cache for import and export mimetypes QStringList KisImportExportManager::m_importMimeTypes; QStringList KisImportExportManager::m_exportMimeTypes; class Q_DECL_HIDDEN KisImportExportManager::Private { public: KoUpdaterPtr updater; QString cachedExportFilterMimeType; QSharedPointer cachedExportFilter; }; struct KisImportExportManager::ConversionResult { ConversionResult() { } ConversionResult(const QFuture &futureStatus) : m_isAsync(true), m_futureStatus(futureStatus) { } ConversionResult(KisImportExportErrorCode status) : m_isAsync(false), m_status(status) { } bool isAsync() const { return m_isAsync; } QFuture futureStatus() const { // if the result is not async, then it means some failure happened, // just return a cancelled future KIS_SAFE_ASSERT_RECOVER_NOOP(m_isAsync || !m_status.isOk()); return m_futureStatus; } KisImportExportErrorCode status() const { return m_status; } void setStatus(KisImportExportErrorCode value) { m_status = value; } private: bool m_isAsync = false; QFuture m_futureStatus; KisImportExportErrorCode m_status = ImportExportCodes::InternalError; }; KisImportExportManager::KisImportExportManager(KisDocument* document) : m_document(document) , d(new Private) { } KisImportExportManager::~KisImportExportManager() { delete d; } KisImportExportErrorCode KisImportExportManager::importDocument(const QString& location, const QString& mimeType) { ConversionResult result = convert(Import, location, location, mimeType, false, 0, false); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!result.isAsync(), ImportExportCodes::InternalError); return result.status(); } KisImportExportErrorCode KisImportExportManager::exportDocument(const QString& location, const QString& realLocation, const QByteArray& mimeType, bool showWarnings, KisPropertiesConfigurationSP exportConfiguration) { ConversionResult result = convert(Export, location, realLocation, mimeType, showWarnings, exportConfiguration, false); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!result.isAsync(), ImportExportCodes::InternalError); return result.status(); } QFuture KisImportExportManager::exportDocumentAsyc(const QString &location, const QString &realLocation, const QByteArray &mimeType, KisImportExportErrorCode &status, bool showWarnings, KisPropertiesConfigurationSP exportConfiguration) { ConversionResult result = convert(Export, location, realLocation, mimeType, showWarnings, exportConfiguration, true); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(result.isAsync() || !result.status().isOk(), QFuture()); status = result.status(); return result.futureStatus(); } // The static method to figure out to which parts of the // graph this mimetype has a connection to. QStringList KisImportExportManager::supportedMimeTypes(Direction direction) { // Find the right mimetype by the extension QSet mimeTypes; // mimeTypes << KisDocument::nativeFormatMimeType() << "application/x-krita-paintoppreset" << "image/openraster"; if (direction == KisImportExportManager::Import) { if (m_importMimeTypes.isEmpty()) { QListlist = KoJsonTrader::instance()->query("Krita/FileFilter", ""); Q_FOREACH(QPluginLoader *loader, list) { QJsonObject json = loader->metaData().value("MetaData").toObject(); Q_FOREACH(const QString &mimetype, json.value("X-KDE-Import").toString().split(",", QString::SkipEmptyParts)) { //qDebug() << "Adding import mimetype" << mimetype << KisMimeDatabase::descriptionForMimeType(mimetype) << "from plugin" << loader; mimeTypes << mimetype; } } qDeleteAll(list); m_importMimeTypes = mimeTypes.toList(); } return m_importMimeTypes; } else if (direction == KisImportExportManager::Export) { if (m_exportMimeTypes.isEmpty()) { QListlist = KoJsonTrader::instance()->query("Krita/FileFilter", ""); Q_FOREACH(QPluginLoader *loader, list) { QJsonObject json = loader->metaData().value("MetaData").toObject(); Q_FOREACH(const QString &mimetype, json.value("X-KDE-Export").toString().split(",", QString::SkipEmptyParts)) { //qDebug() << "Adding export mimetype" << mimetype << KisMimeDatabase::descriptionForMimeType(mimetype) << "from plugin" << loader; mimeTypes << mimetype; } } qDeleteAll(list); m_exportMimeTypes = mimeTypes.toList(); } return m_exportMimeTypes; } return QStringList(); } KisImportExportFilter *KisImportExportManager::filterForMimeType(const QString &mimetype, KisImportExportManager::Direction direction) { int weight = -1; KisImportExportFilter *filter = 0; QListlist = KoJsonTrader::instance()->query("Krita/FileFilter", ""); Q_FOREACH(QPluginLoader *loader, list) { QJsonObject json = loader->metaData().value("MetaData").toObject(); QString directionKey = direction == Export ? "X-KDE-Export" : "X-KDE-Import"; if (json.value(directionKey).toString().split(",", QString::SkipEmptyParts).contains(mimetype)) { KLibFactory *factory = qobject_cast(loader->instance()); if (!factory) { warnUI << loader->errorString(); continue; } QObject* obj = factory->create(0); if (!obj || !obj->inherits("KisImportExportFilter")) { delete obj; continue; } KisImportExportFilter *f = qobject_cast(obj); if (!f) { delete obj; continue; } int w = json.value("X-KDE-Weight").toInt(); if (w > weight) { delete filter; filter = f; f->setObjectName(loader->fileName()); weight = w; } } } qDeleteAll(list); if (filter) { filter->setMimeType(mimetype); } return filter; } bool KisImportExportManager::batchMode(void) const { return m_document->fileBatchMode(); } void KisImportExportManager::setUpdater(KoUpdaterPtr updater) { d->updater = updater; } QString KisImportExportManager::askForAudioFileName(const QString &defaultDir, QWidget *parent) { KoFileDialog dialog(parent, KoFileDialog::ImportFiles, "ImportAudio"); if (!defaultDir.isEmpty()) { dialog.setDefaultDir(defaultDir); } QStringList mimeTypes; mimeTypes << "audio/mpeg"; mimeTypes << "audio/ogg"; mimeTypes << "audio/vorbis"; mimeTypes << "audio/vnd.wave"; mimeTypes << "audio/flac"; dialog.setMimeTypeFilters(mimeTypes); - dialog.setCaption(i18nc("@titile:window", "Open Audio")); + dialog.setCaption(i18nc("@title:window", "Open Audio")); return dialog.filename(); } KisImportExportManager::ConversionResult KisImportExportManager::convert(KisImportExportManager::Direction direction, const QString &location, const QString& realLocation, const QString &mimeType, bool showWarnings, KisPropertiesConfigurationSP exportConfiguration, bool isAsync) { // export configuration is supported for export only KIS_SAFE_ASSERT_RECOVER_NOOP(direction == Export || !bool(exportConfiguration)); QString typeName = mimeType; if (typeName.isEmpty()) { typeName = KisMimeDatabase::mimeTypeForFile(location, direction == KisImportExportManager::Export ? false : true); } QSharedPointer filter; /** * Fetching a filter from the registry is a really expensive operation, * because it blocks all the threads. Cache the filter if possible. */ if (direction == KisImportExportManager::Export && d->cachedExportFilter && d->cachedExportFilterMimeType == typeName) { filter = d->cachedExportFilter; } else { filter = toQShared(filterForMimeType(typeName, direction)); if (direction == Export) { d->cachedExportFilter = filter; d->cachedExportFilterMimeType = typeName; } } if (!filter) { return KisImportExportErrorCode(ImportExportCodes::FileFormatIncorrect); } filter->setFilename(location); filter->setRealFilename(realLocation); filter->setBatchMode(batchMode()); filter->setMimeType(typeName); if (!d->updater.isNull()) { // WARNING: The updater is not guaranteed to be persistent! If you ever want // to add progress reporting to "Save also as .kra", make sure you create // a separate KoProgressUpdater for that! // WARNING2: the failsafe completion of the updater happens in the destructor // the filter. filter->setUpdater(d->updater); } QByteArray from, to; if (direction == Export) { from = m_document->nativeFormatMimeType(); to = typeName.toLatin1(); } else { from = typeName.toLatin1(); to = m_document->nativeFormatMimeType(); } KIS_ASSERT_RECOVER_RETURN_VALUE( direction == Import || direction == Export, KisImportExportErrorCode(ImportExportCodes::InternalError)); // "bad conversion graph" ConversionResult result = KisImportExportErrorCode(ImportExportCodes::OK); if (direction == Import) { KisUsageLogger::log(QString("Importing %1 to %2. Location: %3. Real location: %4. Batchmode: %5") .arg(QString::fromLatin1(from)) .arg(QString::fromLatin1(to)) .arg(location) .arg(realLocation) .arg(batchMode())); // async importing is not yet supported! KIS_SAFE_ASSERT_RECOVER_NOOP(!isAsync); // FIXME: Dmitry says "this progress reporting code never worked. Initial idea was to implement it his way, but I stopped and didn't finish it" if (0 && !batchMode()) { KisAsyncActionFeedback f(i18n("Opening document..."), 0); result = f.runAction(std::bind(&KisImportExportManager::doImport, this, location, filter)); } else { result = doImport(location, filter); } if (result.status().isOk()) { KisImageSP image = m_document->image(); KisUsageLogger::log(QString("Loaded image from %1. Size: %2 * %3 pixels, %4 dpi. Color model: %6 %5 (%7). Layers: %8") .arg(QString::fromLatin1(from)) .arg(image->width()) .arg(image->height()) .arg(image->xRes()) .arg(image->colorSpace()->colorModelId().name()) .arg(image->colorSpace()->colorDepthId().name()) .arg(image->colorSpace()->profile()->name()) .arg(image->nlayers())); } else { KisUsageLogger::log(QString("Failed to load image from %1").arg(QString::fromLatin1(from))); } } else /* if (direction == Export) */ { if (!exportConfiguration) { exportConfiguration = filter->lastSavedConfiguration(from, to); } if (exportConfiguration) { fillStaticExportConfigurationProperties(exportConfiguration); } bool alsoAsKra = false; bool askUser = askUserAboutExportConfiguration(filter, exportConfiguration, from, to, batchMode(), showWarnings, &alsoAsKra); if (!batchMode() && !askUser) { return KisImportExportErrorCode(ImportExportCodes::Cancelled); } KisUsageLogger::log(QString("Converting from %1 to %2. Location: %3. Real location: %4. Batchmode: %5. Configuration: %6") .arg(QString::fromLatin1(from)) .arg(QString::fromLatin1(to)) .arg(location) .arg(realLocation) .arg(batchMode()) .arg(exportConfiguration ? exportConfiguration->toXML() : "none")); if (isAsync) { result = QtConcurrent::run(std::bind(&KisImportExportManager::doExport, this, location, filter, exportConfiguration, alsoAsKra)); // we should explicitly report that the exporting has been initiated result.setStatus(ImportExportCodes::OK); } else if (!batchMode()) { KisAsyncActionFeedback f(i18n("Saving document..."), 0); result = f.runAction(std::bind(&KisImportExportManager::doExport, this, location, filter, exportConfiguration, alsoAsKra)); } else { result = doExport(location, filter, exportConfiguration, alsoAsKra); } if (exportConfiguration && !batchMode() && showWarnings) { KisConfig(false).setExportConfiguration(typeName, exportConfiguration); } } return result; } void KisImportExportManager::fillStaticExportConfigurationProperties(KisPropertiesConfigurationSP exportConfiguration, KisImageSP image) { KisPaintDeviceSP dev = image->projection(); const KoColorSpace* cs = dev->colorSpace(); const bool isThereAlpha = KisPainter::checkDeviceHasTransparency(image->projection()); exportConfiguration->setProperty(KisImportExportFilter::ImageContainsTransparencyTag, isThereAlpha); exportConfiguration->setProperty(KisImportExportFilter::ColorModelIDTag, cs->colorModelId().id()); exportConfiguration->setProperty(KisImportExportFilter::ColorDepthIDTag, cs->colorDepthId().id()); const bool sRGB = (cs->profile()->name().contains(QLatin1String("srgb"), Qt::CaseInsensitive) && !cs->profile()->name().contains(QLatin1String("g10"))); exportConfiguration->setProperty(KisImportExportFilter::sRGBTag, sRGB); } void KisImportExportManager::fillStaticExportConfigurationProperties(KisPropertiesConfigurationSP exportConfiguration) { return fillStaticExportConfigurationProperties(exportConfiguration, m_document->image()); } bool KisImportExportManager::askUserAboutExportConfiguration( QSharedPointer filter, KisPropertiesConfigurationSP exportConfiguration, const QByteArray &from, const QByteArray &to, const bool batchMode, const bool showWarnings, bool *alsoAsKra) { // prevents the animation renderer from running this code const QString mimeUserDescription = KisMimeDatabase::descriptionForMimeType(to); QStringList warnings; QStringList errors; { KisPreExportChecker checker; checker.check(m_document->image(), filter->exportChecks()); warnings = checker.warnings(); errors = checker.errors(); } KisConfigWidget *wdg = 0; if (QThread::currentThread() == qApp->thread()) { wdg = filter->createConfigurationWidget(0, from, to); KisMainWindow *kisMain = KisPart::instance()->currentMainwindow(); if (wdg && kisMain) { KisViewManager *manager = kisMain->viewManager(); wdg->setView(manager); } } // Extra checks that cannot be done by the checker, because the checker only has access to the image. if (!m_document->assistants().isEmpty() && to != m_document->nativeFormatMimeType()) { warnings.append(i18nc("image conversion warning", "The image contains assistants. The assistants will not be saved.")); } if (m_document->referenceImagesLayer() && m_document->referenceImagesLayer()->shapeCount() > 0 && to != m_document->nativeFormatMimeType()) { warnings.append(i18nc("image conversion warning", "The image contains reference images. The reference images will not be saved.")); } if (m_document->guidesConfig().hasGuides() && to != m_document->nativeFormatMimeType()) { warnings.append(i18nc("image conversion warning", "The image contains guides. The guides will not be saved.")); } if (!m_document->gridConfig().isDefault() && to != m_document->nativeFormatMimeType()) { warnings.append(i18nc("image conversion warning", "The image contains a custom grid configuration. The configuration will not be saved.")); } if (!batchMode && !errors.isEmpty()) { QString error = "

" + i18n("Error: cannot save this image as a %1.", mimeUserDescription) + " " + i18n("Reasons:") + "

" + "

    "; Q_FOREACH(const QString &w, errors) { error += "\n
  • " + w + "
  • "; } error += "
"; QMessageBox::critical(KisPart::instance()->currentMainwindow(), i18nc("@title:window", "Krita: Export Error"), error); return false; } if (!batchMode && (wdg || !warnings.isEmpty())) { KoDialog dlg; dlg.setButtons(KoDialog::Ok | KoDialog::Cancel); dlg.setWindowTitle(mimeUserDescription); QWidget *page = new QWidget(&dlg); QVBoxLayout *layout = new QVBoxLayout(page); if (showWarnings && !warnings.isEmpty()) { QHBoxLayout *hLayout = new QHBoxLayout(); QLabel *labelWarning = new QLabel(); labelWarning->setPixmap(KisIconUtils::loadIcon("warning").pixmap(32, 32)); hLayout->addWidget(labelWarning); KisPopupButton *bn = new KisPopupButton(0); bn->setText(i18nc("Keep the extra space at the end of the sentence, please", "Warning: saving as %1 will lose information from your image. ", mimeUserDescription)); hLayout->addWidget(bn); layout->addLayout(hLayout); QTextBrowser *browser = new QTextBrowser(); browser->setMinimumWidth(bn->width()); bn->setPopupWidget(browser); QString warning = "

" + i18n("You will lose information when saving this image as a %1.", mimeUserDescription); if (warnings.size() == 1) { warning += " " + i18n("Reason:") + "

"; } else { warning += " " + i18n("Reasons:") + "

"; } warning += "

    "; Q_FOREACH(const QString &w, warnings) { warning += "\n
  • " + w + "
  • "; } warning += "
"; browser->setHtml(warning); } if (wdg) { QGroupBox *box = new QGroupBox(i18n("Options")); QVBoxLayout *boxLayout = new QVBoxLayout(box); wdg->setConfiguration(exportConfiguration); boxLayout->addWidget(wdg); layout->addWidget(box); } QCheckBox *chkAlsoAsKra = 0; if (showWarnings && !warnings.isEmpty()) { chkAlsoAsKra = new QCheckBox(i18n("Also save your image as a Krita file.")); chkAlsoAsKra->setChecked(KisConfig(true).readEntry("AlsoSaveAsKra", false)); layout->addWidget(chkAlsoAsKra); } dlg.setMainWidget(page); dlg.resize(dlg.minimumSize()); if (showWarnings || wdg) { if (!dlg.exec()) { return false; } } *alsoAsKra = false; if (chkAlsoAsKra) { KisConfig(false).writeEntry("AlsoSaveAsKra", chkAlsoAsKra->isChecked()); *alsoAsKra = chkAlsoAsKra->isChecked(); } if (wdg) { *exportConfiguration = *wdg->configuration(); } } return true; } KisImportExportErrorCode KisImportExportManager::doImport(const QString &location, QSharedPointer filter) { QFile file(location); if (!file.exists()) { return ImportExportCodes::FileNotExist; } if (filter->supportsIO() && !file.open(QFile::ReadOnly)) { return KisImportExportErrorCode(KisImportExportErrorCannotRead(file.error())); } KisImportExportErrorCode status = filter->convert(m_document, &file, KisPropertiesConfigurationSP()); if (file.isOpen()) { file.close(); } return status; } KisImportExportErrorCode KisImportExportManager::doExport(const QString &location, QSharedPointer filter, KisPropertiesConfigurationSP exportConfiguration, bool alsoAsKra) { KisImportExportErrorCode status = doExportImpl(location, filter, exportConfiguration); if (alsoAsKra && status.isOk()) { QString kraLocation = location + ".kra"; QByteArray mime = m_document->nativeFormatMimeType(); QSharedPointer filter( filterForMimeType(QString::fromLatin1(mime), Export)); KIS_SAFE_ASSERT_RECOVER_NOOP(filter); if (filter) { filter->setFilename(kraLocation); KisPropertiesConfigurationSP kraExportConfiguration = filter->lastSavedConfiguration(mime, mime); status = doExportImpl(kraLocation, filter, kraExportConfiguration); } else { status = ImportExportCodes::FileFormatIncorrect; } } return status; } // Temporary workaround until QTBUG-57299 is fixed. // 02-10-2019 update: the bug is closed, but we've still seen this issue. // and without using QSaveFile the issue can still occur // when QFile::copy fails because Dropbox/Google/OneDrive // locks the target file. #ifndef Q_OS_WIN #define USE_QSAVEFILE #endif KisImportExportErrorCode KisImportExportManager::doExportImpl(const QString &location, QSharedPointer filter, KisPropertiesConfigurationSP exportConfiguration) { #ifdef USE_QSAVEFILE QSaveFile file(location); file.setDirectWriteFallback(true); if (filter->supportsIO() && !file.open(QFile::WriteOnly)) { #else QFileInfo fi(location); QTemporaryFile file(fi.absolutePath() + ".XXXXXX.kra"); if (filter->supportsIO() && !file.open()) { #endif KisImportExportErrorCannotWrite result(file.error()); #ifdef USE_QSAVEFILE file.cancelWriting(); #endif return result; } KisImportExportErrorCode status = filter->convert(m_document, &file, exportConfiguration); if (filter->supportsIO()) { if (!status.isOk()) { #ifdef USE_QSAVEFILE file.cancelWriting(); #endif } else { #ifdef USE_QSAVEFILE if (!file.commit()) { qWarning() << "Could not commit QSaveFile"; status = KisImportExportErrorCannotWrite(file.error()); } #else file.flush(); file.close(); QFile target(location); if (target.exists()) { // There should already be a .kra~ backup target.remove(); } if (!file.copy(location)) { file.setAutoRemove(false); return KisImportExportErrorCannotWrite(file.error()); } #endif } } // Do some minimal verification QString verificationResult = filter->verify(location); if (!verificationResult.isEmpty()) { status = KisImportExportErrorCode(ImportExportCodes::ErrorWhileWriting); m_document->setErrorMessage(verificationResult); } return status; } #include diff --git a/libs/ui/KisMainWindow.cpp b/libs/ui/KisMainWindow.cpp index c7729692e8..fd6505c954 100644 --- a/libs/ui/KisMainWindow.cpp +++ b/libs/ui/KisMainWindow.cpp @@ -1,2763 +1,2763 @@ /* This file is part of the KDE project Copyright (C) 1998, 1999 Torben Weis Copyright (C) 2000-2006 David Faure Copyright (C) 2007, 2009 Thomas zander 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 "KisMainWindow.h" #include // qt includes #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 #include #include #include #include #include #include #include #include #include #include #include "kis_selection_manager.h" #include "kis_icon_utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "KoDockFactoryBase.h" #include "KoDocumentInfoDlg.h" #include "KoDocumentInfo.h" #include "KoFileDialog.h" #include #include #include #include #include #include "KoToolDocker.h" #include "KoToolBoxDocker_p.h" #include #include #include #include #include #include #ifdef Q_OS_ANDROID #include #endif #include #include "dialogs/kis_about_application.h" #include "dialogs/kis_delayed_save_dialog.h" #include "dialogs/kis_dlg_preferences.h" #include "kis_action_manager.h" #include "KisApplication.h" #include "kis_canvas2.h" #include "kis_canvas_controller.h" #include "kis_canvas_resource_provider.h" #include "kis_clipboard.h" #include "kis_config.h" #include "kis_config_notifier.h" #include "kis_custom_image_widget.h" #include #include "kis_group_layer.h" #include "kis_image_from_clipboard_widget.h" #include "kis_image.h" #include #include "KisImportExportManager.h" #include "kis_mainwindow_observer.h" #include "kis_memory_statistics_server.h" #include "kis_node.h" #include "KisOpenPane.h" #include "kis_paintop_box.h" #include "KisPart.h" #include "KisPrintJob.h" #include "KisResourceServerProvider.h" #include "kis_signal_compressor_with_param.h" #include "kis_statusbar.h" #include "KisView.h" #include "KisViewManager.h" #include "thememanager.h" #include "kis_animation_importer.h" #include "dialogs/kis_dlg_import_image_sequence.h" #include #include "KisWindowLayoutManager.h" #include #include "KisWelcomePageWidget.h" #include #include #include "KisCanvasWindow.h" #include "kis_action.h" #include class ToolDockerFactory : public KoDockFactoryBase { public: ToolDockerFactory() : KoDockFactoryBase() { } QString id() const override { return "sharedtooldocker"; } QDockWidget* createDockWidget() override { KoToolDocker* dockWidget = new KoToolDocker(); return dockWidget; } DockPosition defaultDockPosition() const override { return DockRight; } }; class Q_DECL_HIDDEN KisMainWindow::Private { public: Private(KisMainWindow *parent, QUuid id) : q(parent) , id(id) , dockWidgetMenu(new KActionMenu(i18nc("@action:inmenu", "&Dockers"), parent)) , windowMenu(new KActionMenu(i18nc("@action:inmenu", "&Window"), parent)) , documentMenu(new KActionMenu(i18nc("@action:inmenu", "New &View"), parent)) , workspaceMenu(new KActionMenu(i18nc("@action:inmenu", "Wor&kspace"), parent)) , welcomePage(new KisWelcomePageWidget(parent)) , widgetStack(new QStackedWidget(parent)) , mdiArea(new QMdiArea(parent)) , windowMapper(new KisSignalMapper(parent)) , documentMapper(new KisSignalMapper(parent)) #ifdef Q_OS_ANDROID , fileManager(new KisAndroidFileManager(parent)) #endif { if (id.isNull()) this->id = QUuid::createUuid(); welcomeScroller = new QScrollArea(); welcomeScroller->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); welcomeScroller->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); welcomeScroller->setWidget(welcomePage); welcomeScroller->setWidgetResizable(true); widgetStack->addWidget(welcomeScroller); widgetStack->addWidget(mdiArea); mdiArea->setTabsMovable(true); mdiArea->setActivationOrder(QMdiArea::ActivationHistoryOrder); } ~Private() { qDeleteAll(toolbarList); } KisMainWindow *q {0}; QUuid id; KisViewManager *viewManager {0}; QPointer activeView; QList toolbarList; bool firstTime {true}; bool windowSizeDirty {false}; bool readOnly {false}; KisAction *showDocumentInfo {0}; KisAction *saveAction {0}; KisAction *saveActionAs {0}; // KisAction *printAction; // KisAction *printActionPreview; // KisAction *exportPdf {0}; KisAction *importAnimation {0}; KisAction *closeAll {0}; // KisAction *reloadFile; KisAction *importFile {0}; KisAction *exportFile {0}; KisAction *undo {0}; KisAction *redo {0}; KisAction *newWindow {0}; KisAction *close {0}; KisAction *mdiCascade {0}; KisAction *mdiTile {0}; KisAction *mdiNextWindow {0}; KisAction *mdiPreviousWindow {0}; KisAction *toggleDockers {0}; KisAction *toggleDockerTitleBars {0}; KisAction *toggleDetachCanvas {0}; KisAction *fullScreenMode {0}; KisAction *showSessionManager {0}; KisAction *expandingSpacers[2]; KActionMenu *dockWidgetMenu; KActionMenu *windowMenu; KActionMenu *documentMenu; KActionMenu *workspaceMenu; KHelpMenu *helpMenu {0}; KRecentFilesAction *recentFiles {0}; KoResourceModel *workspacemodel {0}; QScopedPointer undoActionsUpdateManager; QString lastExportLocation; QMap dockWidgetsMap; QByteArray dockerStateBeforeHiding; KoToolDocker *toolOptionsDocker {0}; QCloseEvent *deferredClosingEvent {0}; Digikam::ThemeManager *themeManager {0}; QScrollArea *welcomeScroller {0}; KisWelcomePageWidget *welcomePage {0}; QStackedWidget *widgetStack {0}; QMdiArea *mdiArea; QMdiSubWindow *activeSubWindow {0}; KisSignalMapper *windowMapper; KisSignalMapper *documentMapper; KisCanvasWindow *canvasWindow {0}; QByteArray lastExportedFormat; QScopedPointer > tabSwitchCompressor; QMutex savingEntryMutex; KConfigGroup windowStateConfig; QUuid workspaceBorrowedBy; KisSignalAutoConnectionsStore screenConnectionsStore; #ifdef Q_OS_ANDROID KisAndroidFileManager *fileManager; #endif KisActionManager * actionManager() { return viewManager->actionManager(); } QTabBar* findTabBarHACK() { QObjectList objects = mdiArea->children(); Q_FOREACH (QObject *object, objects) { QTabBar *bar = qobject_cast(object); if (bar) { return bar; } } return 0; } }; KisMainWindow::KisMainWindow(QUuid uuid) : KXmlGuiWindow() , d(new Private(this, uuid)) { auto rserver = KisResourceServerProvider::instance()->workspaceServer(); QSharedPointer adapter(new KoResourceServerAdapter(rserver)); d->workspacemodel = new KoResourceModel(adapter, this); connect(d->workspacemodel, &KoResourceModel::afterResourcesLayoutReset, this, [&]() { updateWindowMenu(); }); d->viewManager = new KisViewManager(this, actionCollection()); KConfigGroup group( KSharedConfig::openConfig(), "theme"); d->themeManager = new Digikam::ThemeManager(group.readEntry("Theme", "Krita dark"), this); d->windowStateConfig = KSharedConfig::openConfig()->group("MainWindow"); setStandardToolBarMenuEnabled(true); setTabPosition(Qt::AllDockWidgetAreas, QTabWidget::North); setDockNestingEnabled(true); qApp->setStartDragDistance(25); // 25 px is a distance that works well for Tablet and Mouse events #ifdef Q_OS_MACOS setUnifiedTitleAndToolBarOnMac(true); #endif connect(this, SIGNAL(restoringDone()), this, SLOT(forceDockTabFonts())); connect(this, SIGNAL(themeChanged()), d->viewManager, SLOT(updateIcons())); connect(KisPart::instance(), SIGNAL(documentClosed(QString)), SLOT(updateWindowMenu())); connect(KisPart::instance(), SIGNAL(documentOpened(QString)), SLOT(updateWindowMenu())); connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), this, SLOT(configChanged())); actionCollection()->addAssociatedWidget(this); KoPluginLoader::instance()->load("Krita/ViewPlugin", "Type == 'Service' and ([X-Krita-Version] == 28)", KoPluginLoader::PluginsConfig(), d->viewManager, false); // Load the per-application plugins (Right now, only Python) We do this only once, when the first mainwindow is being created. KoPluginLoader::instance()->load("Krita/ApplicationPlugin", "Type == 'Service' and ([X-Krita-Version] == 28)", KoPluginLoader::PluginsConfig(), qApp, true); KoToolBoxFactory toolBoxFactory; QDockWidget *toolbox = createDockWidget(&toolBoxFactory); toolbox->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable); KisConfig cfg(true); if (cfg.toolOptionsInDocker()) { ToolDockerFactory toolDockerFactory; d->toolOptionsDocker = qobject_cast(createDockWidget(&toolDockerFactory)); d->toolOptionsDocker->toggleViewAction()->setEnabled(true); } QMap dockwidgetActions; dockwidgetActions[toolbox->toggleViewAction()->text()] = toolbox->toggleViewAction(); Q_FOREACH (const QString & docker, KoDockRegistry::instance()->keys()) { KoDockFactoryBase *factory = KoDockRegistry::instance()->value(docker); QDockWidget *dw = createDockWidget(factory); dockwidgetActions[dw->toggleViewAction()->text()] = dw->toggleViewAction(); } if (d->toolOptionsDocker) { dockwidgetActions[d->toolOptionsDocker->toggleViewAction()->text()] = d->toolOptionsDocker->toggleViewAction(); } connect(KoToolManager::instance(), SIGNAL(toolOptionWidgetsChanged(KoCanvasController*,QList >)), this, SLOT(newOptionWidgets(KoCanvasController*,QList >))); Q_FOREACH (QString title, dockwidgetActions.keys()) { d->dockWidgetMenu->addAction(dockwidgetActions[title]); } Q_FOREACH (QDockWidget *wdg, dockWidgets()) { if ((wdg->features() & QDockWidget::DockWidgetClosable) == 0) { wdg->setVisible(true); } } Q_FOREACH (KoCanvasObserverBase* observer, canvasObservers()) { observer->setObservedCanvas(0); KisMainwindowObserver* mainwindowObserver = dynamic_cast(observer); if (mainwindowObserver) { mainwindowObserver->setViewManager(d->viewManager); } } // Load all the actions from the tool plugins Q_FOREACH(KoToolFactoryBase *toolFactory, KoToolRegistry::instance()->values()) { toolFactory->createActions(actionCollection()); } d->mdiArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); d->mdiArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); d->mdiArea->setTabPosition(QTabWidget::North); d->mdiArea->setTabsClosable(true); // Tab close button override // Windows just has a black X, and Ubuntu has a dark x that is hard to read // just switch this icon out for all OSs so it is easier to see d->mdiArea->setStyleSheet("QTabBar::close-button { image: url(:/pics/broken-preset.png) }"); setCentralWidget(d->widgetStack); d->widgetStack->setCurrentIndex(0); connect(d->mdiArea, SIGNAL(subWindowActivated(QMdiSubWindow*)), this, SLOT(subWindowActivated())); connect(d->windowMapper, SIGNAL(mapped(QWidget*)), this, SLOT(setActiveSubWindow(QWidget*))); connect(d->documentMapper, SIGNAL(mapped(QObject*)), this, SLOT(newView(QObject*))); d->canvasWindow = new KisCanvasWindow(this); actionCollection()->addAssociatedWidget(d->canvasWindow); createActions(); // the welcome screen needs to grab actions...so make sure this line goes after the createAction() so they exist d->welcomePage->setMainWindow(this); setAutoSaveSettings(d->windowStateConfig, false); subWindowActivated(); updateWindowMenu(); if (isHelpMenuEnabled() && !d->helpMenu) { // workaround for KHelpMenu (or rather KAboutData::applicationData()) internally // not using the Q*Application metadata ATM, which results e.g. in the bugreport wizard // not having the app version preset // fixed hopefully in KF5 5.22.0, patch pending QGuiApplication *app = qApp; KAboutData aboutData(app->applicationName(), app->applicationDisplayName(), app->applicationVersion()); aboutData.setOrganizationDomain(app->organizationDomain().toUtf8()); d->helpMenu = new KHelpMenu(this, aboutData, false); // workaround-less version: // d->helpMenu = new KHelpMenu(this, QString()/*unused*/, false); // The difference between using KActionCollection->addAction() is that // these actions do not get tied to the MainWindow. What does this all do? KActionCollection *actions = d->viewManager->actionCollection(); QAction *helpContentsAction = d->helpMenu->action(KHelpMenu::menuHelpContents); QAction *whatsThisAction = d->helpMenu->action(KHelpMenu::menuWhatsThis); QAction *reportBugAction = d->helpMenu->action(KHelpMenu::menuReportBug); QAction *switchLanguageAction = d->helpMenu->action(KHelpMenu::menuSwitchLanguage); QAction *aboutAppAction = d->helpMenu->action(KHelpMenu::menuAboutApp); QAction *aboutKdeAction = d->helpMenu->action(KHelpMenu::menuAboutKDE); if (helpContentsAction) { actions->addAction(helpContentsAction->objectName(), helpContentsAction); } if (whatsThisAction) { actions->addAction(whatsThisAction->objectName(), whatsThisAction); } if (reportBugAction) { actions->addAction(reportBugAction->objectName(), reportBugAction); } if (switchLanguageAction) { actions->addAction(switchLanguageAction->objectName(), switchLanguageAction); } if (aboutAppAction) { actions->addAction(aboutAppAction->objectName(), aboutAppAction); } if (aboutKdeAction) { actions->addAction(aboutKdeAction->objectName(), aboutKdeAction); } connect(d->helpMenu, SIGNAL(showAboutApplication()), SLOT(showAboutApplication())); } // KDE' libs 4''s help contents action is broken outside kde, for some reason... We can handle it just as easily ourselves QAction *helpAction = actionCollection()->action("help_contents"); helpAction->disconnect(); connect(helpAction, SIGNAL(triggered()), this, SLOT(showManual())); #if 0 //check for colliding shortcuts QSet existingShortcuts; Q_FOREACH (QAction* action, actionCollection()->actions()) { if(action->shortcut() == QKeySequence(0)) { continue; } dbgKrita << "shortcut " << action->text() << " " << action->shortcut(); Q_ASSERT(!existingShortcuts.contains(action->shortcut())); existingShortcuts.insert(action->shortcut()); } #endif configChanged(); // If we have customized the toolbars, load that first setLocalXMLFile(KoResourcePaths::locateLocal("data", "krita4.xmlgui")); setXMLFile(":/kxmlgui5/krita4.xmlgui"); guiFactory()->addClient(this); connect(guiFactory(), SIGNAL(makingChanges(bool)), SLOT(slotXmlGuiMakingChanges(bool))); // Create and plug toolbar list for Settings menu QList toolbarList; Q_FOREACH (QWidget* it, guiFactory()->containers("ToolBar")) { KToolBar * toolBar = ::qobject_cast(it); toolBar->setMovable(KisConfig(true).readEntry("LockAllDockerPanels", false)); if (toolBar) { if (toolBar->objectName() == "BrushesAndStuff") { toolBar->setEnabled(false); } KToggleAction* act = new KToggleAction(i18n("Show %1 Toolbar", toolBar->windowTitle()), this); actionCollection()->addAction(toolBar->objectName().toUtf8(), act); act->setCheckedState(KGuiItem(i18n("Hide %1 Toolbar", toolBar->windowTitle()))); connect(act, SIGNAL(toggled(bool)), this, SLOT(slotToolbarToggled(bool))); act->setChecked(!toolBar->isHidden()); toolbarList.append(act); } else { warnUI << "Toolbar list contains a " << it->metaObject()->className() << " which is not a toolbar!"; } } KToolBar::setToolBarsLocked(KisConfig(true).readEntry("LockAllDockerPanels", false)); plugActionList("toolbarlist", toolbarList); d->toolbarList = toolbarList; applyToolBarLayout(); d->viewManager->updateGUI(); d->viewManager->updateIcons(); QTimer::singleShot(1000, this, SLOT(checkSanity())); { using namespace std::placeholders; // For _1 placeholder std::function callback( std::bind(&KisMainWindow::switchTab, this, _1)); d->tabSwitchCompressor.reset( new KisSignalCompressorWithParam(500, callback, KisSignalCompressor::FIRST_INACTIVE)); } if (cfg.readEntry("CanvasOnlyActive", false)) { QString currentWorkspace = cfg.readEntry("CurrentWorkspace", "Default"); KoResourceServer * rserver = KisResourceServerProvider::instance()->workspaceServer(); KisWorkspaceResource* workspace = rserver->resourceByName(currentWorkspace); if (workspace) { restoreWorkspace(workspace); } cfg.writeEntry("CanvasOnlyActive", false); menuBar()->setVisible(true); } this->winId(); // Ensures the native window has been created. QWindow *window = this->windowHandle(); connect(window, SIGNAL(screenChanged(QScreen *)), this, SLOT(windowScreenChanged(QScreen *))); } KisMainWindow::~KisMainWindow() { // Q_FOREACH (QAction *ac, actionCollection()->actions()) { // QAction *action = qobject_cast(ac); // if (action) { // qDebug() << "", "").replace("", "") // << "\n\ticonText=" << action->iconText().replace("&", "&") // << "\n\tshortcut=" << action->shortcut().toString() // << "\n\tisCheckable=" << QString((action->isChecked() ? "true" : "false")) // << "\n\tstatusTip=" << action->statusTip() // << "\n/>\n" ; // } // else { // dbgKrita << "Got a non-qaction:" << ac->objectName(); // } // } // The doc and view might still exist (this is the case when closing the window) KisPart::instance()->removeMainWindow(this); delete d->viewManager; delete d; } QUuid KisMainWindow::id() const { return d->id; } void KisMainWindow::addView(KisView *view, QMdiSubWindow *subWindow) { if (d->activeView == view && !subWindow) return; if (d->activeView) { d->activeView->disconnect(this); } // register the newly created view in the input manager viewManager()->inputManager()->addTrackedCanvas(view->canvasBase()); showView(view, subWindow); updateCaption(); emit restoringDone(); if (d->activeView) { connect(d->activeView, SIGNAL(titleModified(QString,bool)), SLOT(slotDocumentTitleModified())); connect(d->viewManager->statusBar(), SIGNAL(memoryStatusUpdated()), this, SLOT(updateCaption())); } } void KisMainWindow::notifyChildViewDestroyed(KisView *view) { /** * If we are the last view of the window, Qt will not activate another tab - * before destroying tab/window. In ths case we should clear oll the dangling + * before destroying tab/window. In this case we should clear all the dangling * pointers manually by setting the current view to null */ viewManager()->inputManager()->removeTrackedCanvas(view->canvasBase()); if (view->canvasBase() == viewManager()->canvasBase()) { viewManager()->setCurrentView(0); } } void KisMainWindow::showView(KisView *imageView, QMdiSubWindow *subwin) { if (imageView && activeView() != imageView) { // XXX: find a better way to initialize this! imageView->setViewManager(d->viewManager); imageView->canvasBase()->setFavoriteResourceManager(d->viewManager->paintOpBox()->favoriteResourcesManager()); imageView->slotLoadingFinished(); if (!subwin) { subwin = d->mdiArea->addSubWindow(imageView); } else { subwin->setWidget(imageView); } imageView->setSubWindow(subwin); subwin->setAttribute(Qt::WA_DeleteOnClose, true); connect(subwin, SIGNAL(destroyed()), SLOT(updateWindowMenu())); KisConfig cfg(true); subwin->setOption(QMdiSubWindow::RubberBandMove, cfg.readEntry("mdi_rubberband", cfg.useOpenGL())); subwin->setOption(QMdiSubWindow::RubberBandResize, cfg.readEntry("mdi_rubberband", cfg.useOpenGL())); subwin->setWindowIcon(qApp->windowIcon()); /** * Hack alert! * * Here we explicitly request KoToolManager to emit all the tool * activation signals, to reinitialize the tool options docker. * * That is needed due to a design flaw we have in the * initialization procedure. The tool in the KoToolManager is * initialized in KisView::setViewManager() calls, which * happens early enough. During this call the tool manager * requests KoCanvasControllerWidget to emit the signal to * update the widgets in the tool docker. *But* at that moment * of time the view is not yet connected to the main window, * because it happens in KisViewManager::setCurrentView a bit * later. This fact makes the widgets updating signals be lost * and never reach the tool docker. * * So here we just explicitly call the tool activation stub. */ KoToolManager::instance()->initializeCurrentToolForCanvas(); if (d->mdiArea->subWindowList().size() == 1) { imageView->showMaximized(); } else { imageView->show(); } // No, no, no: do not try to call this _before_ the show() has // been called on the view; only when that has happened is the // opengl context active, and very bad things happen if we tell // the dockers to update themselves with a view if the opengl // context is not active. setActiveView(imageView); updateWindowMenu(); updateCaption(); } } void KisMainWindow::slotPreferences() { if (KisDlgPreferences::editPreferences()) { KisConfigNotifier::instance()->notifyConfigChanged(); KisConfigNotifier::instance()->notifyPixelGridModeChanged(); KisImageConfigNotifier::instance()->notifyConfigChanged(); // XXX: should this be changed for the views in other windows as well? Q_FOREACH (QPointer koview, KisPart::instance()->views()) { KisViewManager *view = qobject_cast(koview); if (view) { // Update the settings for all nodes -- they don't query // KisConfig directly because they need the settings during // compositing, and they don't connect to the config notifier // because nodes are not QObjects (because only one base class // can be a QObject). KisNode* node = dynamic_cast(view->image()->rootLayer().data()); node->updateSettings(); } } updateWindowMenu(); d->viewManager->showHideScrollbars(); } } void KisMainWindow::slotThemeChanged() { // save theme changes instantly KConfigGroup group( KSharedConfig::openConfig(), "theme"); group.writeEntry("Theme", d->themeManager->currentThemeName()); // reload action icons! Q_FOREACH (QAction *action, actionCollection()->actions()) { KisIconUtils::updateIcon(action); } if (d->mdiArea) { d->mdiArea->setPalette(qApp->palette()); for (int i=0; imdiArea->subWindowList().size(); i++) { QMdiSubWindow *window = d->mdiArea->subWindowList().at(i); if (window) { window->setPalette(qApp->palette()); KisView *view = qobject_cast(window->widget()); if (view) { view->slotThemeChanged(qApp->palette()); } } } } emit themeChanged(); } bool KisMainWindow::canvasDetached() const { return centralWidget() != d->widgetStack; } void KisMainWindow::setCanvasDetached(bool detach) { if (detach == canvasDetached()) return; QWidget *outgoingWidget = centralWidget() ? takeCentralWidget() : nullptr; QWidget *incomingWidget = d->canvasWindow->swapMainWidget(outgoingWidget); if (incomingWidget) { setCentralWidget(incomingWidget); } if (detach) { d->canvasWindow->show(); } else { d->canvasWindow->hide(); } } void KisMainWindow::slotFileSelected(QString path) { QString url = path; if (!url.isEmpty()) { bool res = openDocument(QUrl::fromLocalFile(url), Import); if (!res) { warnKrita << "Loading" << url << "failed"; } } } void KisMainWindow::slotEmptyFilePath() { QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("The chosen file's location could not be found. Does it exist?")); } QWidget * KisMainWindow::canvasWindow() const { return d->canvasWindow; } void KisMainWindow::updateReloadFileAction(KisDocument *doc) { Q_UNUSED(doc); // d->reloadFile->setEnabled(doc && !doc->url().isEmpty()); } void KisMainWindow::setReadWrite(bool readwrite) { d->saveAction->setEnabled(readwrite); d->importFile->setEnabled(readwrite); d->readOnly = !readwrite; updateCaption(); } void KisMainWindow::addRecentURL(const QUrl &url) { // Add entry to recent documents list // (call coming from KisDocument because it must work with cmd line, template dlg, file/open, etc.) if (!url.isEmpty()) { bool ok = true; if (url.isLocalFile()) { QString path = url.adjusted(QUrl::StripTrailingSlash).toLocalFile(); const QStringList tmpDirs = KoResourcePaths::resourceDirs("tmp"); for (QStringList::ConstIterator it = tmpDirs.begin() ; ok && it != tmpDirs.end() ; ++it) { if (path.contains(*it)) { ok = false; // it's in the tmp resource } } const QStringList templateDirs = KoResourcePaths::findDirs("templates"); for (QStringList::ConstIterator it = templateDirs.begin() ; ok && it != templateDirs.end() ; ++it) { if (path.contains(*it)) { ok = false; // it's in the templates directory. break; } } } if (ok) { d->recentFiles->addUrl(url); } saveRecentFiles(); } } void KisMainWindow::saveRecentFiles() { // Save list of recent files KSharedConfigPtr config = KSharedConfig::openConfig(); d->recentFiles->saveEntries(config->group("RecentFiles")); config->sync(); // Tell all windows to reload their list, after saving // Doesn't work multi-process, but it's a start Q_FOREACH (KisMainWindow *mw, KisPart::instance()->mainWindows()) { if (mw != this) { mw->reloadRecentFileList(); } } } QList KisMainWindow::recentFilesUrls() { return d->recentFiles->urls(); } void KisMainWindow::clearRecentFiles() { d->recentFiles->clear(); d->welcomePage->populateRecentDocuments(); } void KisMainWindow::removeRecentUrl(const QUrl &url) { d->recentFiles->removeUrl(url); KSharedConfigPtr config = KSharedConfig::openConfig(); d->recentFiles->saveEntries(config->group("RecentFiles")); config->sync(); } void KisMainWindow::reloadRecentFileList() { d->recentFiles->loadEntries(KSharedConfig::openConfig()->group("RecentFiles")); } void KisMainWindow::updateCaption() { if (!d->mdiArea->activeSubWindow()) { updateCaption(QString(), false); } else if (d->activeView && d->activeView->document() && d->activeView->image()){ KisDocument *doc = d->activeView->document(); QString caption(doc->caption()); if (d->readOnly) { caption += " [" + i18n("Write Protected") + "] "; } if (doc->isRecovered()) { caption += " [" + i18n("Recovered") + "] "; } // show the file size for the document KisMemoryStatisticsServer::Statistics m_fileSizeStats = KisMemoryStatisticsServer::instance()->fetchMemoryStatistics(d->activeView ? d->activeView->image() : 0); if (m_fileSizeStats.imageSize) { caption += QString(" (").append( KFormat().formatByteSize(m_fileSizeStats.imageSize)).append( ")"); } updateCaption(caption, doc->isModified()); if (!doc->url().fileName().isEmpty()) { d->saveAction->setToolTip(i18n("Save as %1", doc->url().fileName())); } else { d->saveAction->setToolTip(i18n("Save")); } } } void KisMainWindow::updateCaption(const QString &caption, bool modified) { QString versionString = KritaVersionWrapper::versionString(true); QString title = caption; if (!title.contains(QStringLiteral("[*]"))) { // append the placeholder so that the modified mechanism works title.append(QStringLiteral(" [*]")); } if (d->mdiArea->activeSubWindow()) { #if defined(KRITA_ALPHA) || defined (KRITA_BETA) || defined (KRITA_RC) d->mdiArea->activeSubWindow()->setWindowTitle(QString("%1: %2").arg(versionString).arg(title)); #else d->mdiArea->activeSubWindow()->setWindowTitle(title); #endif d->mdiArea->activeSubWindow()->setWindowModified(modified); } else { #if defined(KRITA_ALPHA) || defined (KRITA_BETA) || defined (KRITA_RC) setWindowTitle(QString("%1: %2").arg(versionString).arg(title)); #else setWindowTitle(title); #endif } setWindowModified(modified); } KisView *KisMainWindow::activeView() const { if (d->activeView) { return d->activeView; } return 0; } bool KisMainWindow::openDocument(const QUrl &url, OpenFlags flags) { if (!QFile(url.toLocalFile()).exists()) { if (!(flags & BatchMode)) { QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("The file %1 does not exist.", url.url())); } d->recentFiles->removeUrl(url); //remove the file from the recent-opened-file-list saveRecentFiles(); return false; } return openDocumentInternal(url, flags); } bool KisMainWindow::openDocumentInternal(const QUrl &url, OpenFlags flags) { if (!url.isLocalFile()) { qWarning() << "KisMainWindow::openDocumentInternal. Not a local file:" << url; return false; } KisDocument *newdoc = KisPart::instance()->createDocument(); if (flags & BatchMode) { newdoc->setFileBatchMode(true); } d->firstTime = true; connect(newdoc, SIGNAL(completed()), this, SLOT(slotLoadCompleted())); connect(newdoc, SIGNAL(canceled(QString)), this, SLOT(slotLoadCanceled(QString))); KisDocument::OpenFlags openFlags = KisDocument::None; if (flags & RecoveryFile) { openFlags |= KisDocument::RecoveryFile; } bool openRet = !(flags & Import) ? newdoc->openUrl(url, openFlags) : newdoc->importDocument(url); if (!openRet) { delete newdoc; return false; } KisPart::instance()->addDocument(newdoc); updateReloadFileAction(newdoc); if (!QFileInfo(url.toLocalFile()).isWritable()) { setReadWrite(false); } return true; } void KisMainWindow::showDocument(KisDocument *document) { Q_FOREACH(QMdiSubWindow *subwindow, d->mdiArea->subWindowList()) { KisView *view = qobject_cast(subwindow->widget()); KIS_SAFE_ASSERT_RECOVER_NOOP(view); if (view) { if (view->document() == document) { setActiveSubWindow(subwindow); return; } } } addViewAndNotifyLoadingCompleted(document); } KisView* KisMainWindow::addViewAndNotifyLoadingCompleted(KisDocument *document, QMdiSubWindow *subWindow) { showWelcomeScreen(false); // see workaround in function header KisView *view = KisPart::instance()->createView(document, d->viewManager, this); addView(view, subWindow); emit guiLoadingFinished(); return view; } QStringList KisMainWindow::showOpenFileDialog(bool isImporting) { KoFileDialog dialog(this, KoFileDialog::ImportFiles, "OpenDocument"); dialog.setDefaultDir(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)); dialog.setMimeTypeFilters(KisImportExportManager::supportedMimeTypes(KisImportExportManager::Import)); dialog.setCaption(isImporting ? i18n("Import Images") : i18n("Open Images")); return dialog.filenames(); } // Separate from openDocument to handle async loading (remote URLs) void KisMainWindow::slotLoadCompleted() { KisDocument *newdoc = qobject_cast(sender()); if (newdoc && newdoc->image()) { addViewAndNotifyLoadingCompleted(newdoc); disconnect(newdoc, SIGNAL(completed()), this, SLOT(slotLoadCompleted())); disconnect(newdoc, SIGNAL(canceled(QString)), this, SLOT(slotLoadCanceled(QString))); emit loadCompleted(); } } void KisMainWindow::slotLoadCanceled(const QString & errMsg) { dbgUI << "KisMainWindow::slotLoadCanceled"; if (!errMsg.isEmpty()) // empty when canceled by user QMessageBox::critical(this, i18nc("@title:window", "Krita"), errMsg); // ... can't delete the document, it's the one who emitted the signal... KisDocument* doc = qobject_cast(sender()); Q_ASSERT(doc); disconnect(doc, SIGNAL(completed()), this, SLOT(slotLoadCompleted())); disconnect(doc, SIGNAL(canceled(QString)), this, SLOT(slotLoadCanceled(QString))); } void KisMainWindow::slotSaveCanceled(const QString &errMsg) { dbgUI << "KisMainWindow::slotSaveCanceled"; if (!errMsg.isEmpty()) // empty when canceled by user QMessageBox::critical(this, i18nc("@title:window", "Krita"), errMsg); slotSaveCompleted(); } void KisMainWindow::slotSaveCompleted() { dbgUI << "KisMainWindow::slotSaveCompleted"; KisDocument* doc = qobject_cast(sender()); Q_ASSERT(doc); disconnect(doc, SIGNAL(completed()), this, SLOT(slotSaveCompleted())); disconnect(doc, SIGNAL(canceled(QString)), this, SLOT(slotSaveCanceled(QString))); if (d->deferredClosingEvent) { KXmlGuiWindow::closeEvent(d->deferredClosingEvent); } } bool KisMainWindow::hackIsSaving() const { StdLockableWrapper wrapper(&d->savingEntryMutex); std::unique_lock> l(wrapper, std::try_to_lock); return !l.owns_lock(); } bool KisMainWindow::installBundle(const QString &fileName) const { QFileInfo from(fileName); QFileInfo to(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/bundles/" + from.fileName()); if (to.exists()) { QFile::remove(to.canonicalFilePath()); } return QFile::copy(fileName, QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/bundles/" + from.fileName()); } bool KisMainWindow::saveDocument(KisDocument *document, bool saveas, bool isExporting) { if (!document) { return true; } /** * Make sure that we cannot enter this method twice! * * The lower level functions may call processEvents() so * double-entry is quite possible to achieve. Here we try to lock * the mutex, and if it is failed, just cancel saving. */ StdLockableWrapper wrapper(&d->savingEntryMutex); std::unique_lock> l(wrapper, std::try_to_lock); if (!l.owns_lock()) return false; // no busy wait for saving because it is dangerous! KisDelayedSaveDialog dlg(document->image(), KisDelayedSaveDialog::SaveDialog, 0, this); dlg.blockIfImageIsBusy(); if (dlg.result() == KisDelayedSaveDialog::Rejected) { return false; } else if (dlg.result() == KisDelayedSaveDialog::Ignored) { QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("You are saving a file while the image is " "still rendering. The saved file may be " "incomplete or corrupted.\n\n" "Please select a location where the original " "file will not be overridden!")); saveas = true; } if (document->isRecovered()) { saveas = true; } if (document->url().isEmpty()) { saveas = true; } connect(document, SIGNAL(completed()), this, SLOT(slotSaveCompleted())); connect(document, SIGNAL(canceled(QString)), this, SLOT(slotSaveCanceled(QString))); QByteArray nativeFormat = document->nativeFormatMimeType(); QByteArray oldMimeFormat = document->mimeType(); QUrl suggestedURL = document->url(); QStringList mimeFilter = KisImportExportManager::supportedMimeTypes(KisImportExportManager::Export); mimeFilter = KisImportExportManager::supportedMimeTypes(KisImportExportManager::Export); if (!mimeFilter.contains(oldMimeFormat)) { dbgUI << "KisMainWindow::saveDocument no export filter for" << oldMimeFormat; // --- don't setOutputMimeType in case the user cancels the Save As // dialog and then tries to just plain Save --- // suggest a different filename extension (yes, we fortunately don't all live in a world of magic :)) QString suggestedFilename = QFileInfo(suggestedURL.toLocalFile()).completeBaseName(); if (!suggestedFilename.isEmpty()) { // ".kra" looks strange for a name suggestedFilename = suggestedFilename + "." + KisMimeDatabase::suffixesForMimeType(KIS_MIME_TYPE).first(); suggestedURL = suggestedURL.adjusted(QUrl::RemoveFilename); suggestedURL.setPath(suggestedURL.path() + suggestedFilename); } // force the user to choose outputMimeType saveas = true; } bool ret = false; if (document->url().isEmpty() || isExporting || saveas) { // if you're just File/Save As'ing to change filter options you // don't want to be reminded about overwriting files etc. bool justChangingFilterOptions = false; KoFileDialog dialog(this, KoFileDialog::SaveFile, "SaveAs"); dialog.setCaption(isExporting ? i18n("Exporting") : i18n("Saving As")); //qDebug() << ">>>>>" << isExporting << d->lastExportLocation << d->lastExportedFormat << QString::fromLatin1(document->mimeType()); if (isExporting && !d->lastExportLocation.isEmpty() && !d->lastExportLocation.contains(QDir::tempPath())) { // Use the location where we last exported to, if it's set, as the opening location for the file dialog QString proposedPath = QFileInfo(d->lastExportLocation).absolutePath(); // If the document doesn't have a filename yet, use the title QString proposedFileName = suggestedURL.isEmpty() ? document->documentInfo()->aboutInfo("title") : QFileInfo(suggestedURL.toLocalFile()).completeBaseName(); // Use the last mimetype we exported to by default QString proposedMimeType = d->lastExportedFormat.isEmpty() ? "" : d->lastExportedFormat; QString proposedExtension = KisMimeDatabase::suffixesForMimeType(proposedMimeType).first().remove("*,"); // Set the default dir: this overrides the one loaded from the config file, since we're exporting and the lastExportLocation is not empty dialog.setDefaultDir(proposedPath + "/" + proposedFileName + "." + proposedExtension, true); dialog.setMimeTypeFilters(mimeFilter, proposedMimeType); } else { // Get the last used location for saving KConfigGroup group = KSharedConfig::openConfig()->group("File Dialogs"); QString proposedPath = group.readEntry("SaveAs", ""); // if that is empty, get the last used location for loading if (proposedPath.isEmpty()) { proposedPath = group.readEntry("OpenDocument", ""); } // If that is empty, too, use the Pictures location. if (proposedPath.isEmpty()) { proposedPath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); } // But only use that if the suggestedUrl, that is, the document's own url is empty, otherwise // open the location where the document currently is. dialog.setDefaultDir(suggestedURL.isEmpty() ? proposedPath : suggestedURL.toLocalFile(), true); // If exporting, default to all supported file types if user is exporting QByteArray default_mime_type = ""; if (!isExporting) { // otherwise use the document's mimetype, or if that is empty, kra, which is the savest. default_mime_type = document->mimeType().isEmpty() ? nativeFormat : document->mimeType(); } dialog.setMimeTypeFilters(mimeFilter, QString::fromLatin1(default_mime_type)); } QUrl newURL = QUrl::fromUserInput(dialog.filename()); if (newURL.isLocalFile()) { QString fn = newURL.toLocalFile(); if (QFileInfo(fn).completeSuffix().isEmpty()) { fn.append(KisMimeDatabase::suffixesForMimeType(nativeFormat).first()); newURL = QUrl::fromLocalFile(fn); } } if (document->documentInfo()->aboutInfo("title") == i18n("Unnamed")) { QString fn = newURL.toLocalFile(); QFileInfo info(fn); document->documentInfo()->setAboutInfo("title", info.completeBaseName()); } QByteArray outputFormat = nativeFormat; QString outputFormatString = KisMimeDatabase::mimeTypeForFile(newURL.toLocalFile(), false); outputFormat = outputFormatString.toLatin1(); if (!isExporting) { justChangingFilterOptions = (newURL == document->url()) && (outputFormat == document->mimeType()); } else { QString path = QFileInfo(d->lastExportLocation).absolutePath(); QString filename = QFileInfo(document->url().toLocalFile()).completeBaseName(); justChangingFilterOptions = (QFileInfo(newURL.toLocalFile()).absolutePath() == path) && (QFileInfo(newURL.toLocalFile()).completeBaseName() == filename) && (outputFormat == d->lastExportedFormat); } bool bOk = true; if (newURL.isEmpty()) { bOk = false; } if (bOk) { bool wantToSave = true; // don't change this line unless you know what you're doing :) if (!justChangingFilterOptions) { if (!document->isNativeFormat(outputFormat)) wantToSave = true; } if (wantToSave) { if (!isExporting) { // Save As ret = document->saveAs(newURL, outputFormat, true); if (ret) { dbgUI << "Successful Save As!"; KisPart::instance()->addRecentURLToAllMainWindows(newURL); setReadWrite(true); } else { dbgUI << "Failed Save As!"; } } else { // Export ret = document->exportDocument(newURL, outputFormat); if (ret) { d->lastExportLocation = newURL.toLocalFile(); d->lastExportedFormat = outputFormat; } } } // if (wantToSave) { else ret = false; } // if (bOk) { else ret = false; } else { // saving // We cannot "export" into the currently // opened document. We are not Gimp. KIS_ASSERT_RECOVER_NOOP(!isExporting); // be sure document has the correct outputMimeType! if (document->isModified()) { ret = document->save(true, 0); } if (!ret) { dbgUI << "Failed Save!"; } } updateReloadFileAction(document); updateCaption(); return ret; } void KisMainWindow::undo() { if (activeView()) { activeView()->document()->undoStack()->undo(); } } void KisMainWindow::redo() { if (activeView()) { activeView()->document()->undoStack()->redo(); } } void KisMainWindow::closeEvent(QCloseEvent *e) { if (hackIsSaving()) { e->setAccepted(false); return; } if (!KisPart::instance()->closingSession()) { QAction *action= d->viewManager->actionCollection()->action("view_show_canvas_only"); if ((action) && (action->isChecked())) { action->setChecked(false); } // Save session when last window is closed if (KisPart::instance()->mainwindowCount() == 1) { bool closeAllowed = KisPart::instance()->closeSession(); if (!closeAllowed) { e->setAccepted(false); return; } } } d->mdiArea->closeAllSubWindows(); QList childrenList = d->mdiArea->subWindowList(); if (childrenList.isEmpty()) { d->deferredClosingEvent = e; saveWindowState(true); d->canvasWindow->close(); } else { e->setAccepted(false); } } void KisMainWindow::saveWindowSettings() { KSharedConfigPtr config = KSharedConfig::openConfig(); if (d->windowSizeDirty ) { dbgUI << "KisMainWindow::saveWindowSettings"; KConfigGroup group = d->windowStateConfig; KWindowConfig::saveWindowSize(windowHandle(), group); config->sync(); d->windowSizeDirty = false; } if (!d->activeView || d->activeView->document()) { // Save toolbar position into the config file of the app, under the doc's component name KConfigGroup group = d->windowStateConfig; saveMainWindowSettings(group); // Save collapsible state of dock widgets for (QMap::const_iterator i = d->dockWidgetsMap.constBegin(); i != d->dockWidgetsMap.constEnd(); ++i) { if (i.value()->widget()) { KConfigGroup dockGroup = group.group(QString("DockWidget ") + i.key()); dockGroup.writeEntry("Collapsed", i.value()->widget()->isHidden()); dockGroup.writeEntry("Locked", i.value()->property("Locked").toBool()); dockGroup.writeEntry("DockArea", (int) dockWidgetArea(i.value())); dockGroup.writeEntry("xPosition", (int) i.value()->widget()->x()); dockGroup.writeEntry("yPosition", (int) i.value()->widget()->y()); dockGroup.writeEntry("width", (int) i.value()->widget()->width()); dockGroup.writeEntry("height", (int) i.value()->widget()->height()); } } } KSharedConfig::openConfig()->sync(); resetAutoSaveSettings(); // Don't let KMainWindow override the good stuff we wrote down } void KisMainWindow::resizeEvent(QResizeEvent * e) { d->windowSizeDirty = true; KXmlGuiWindow::resizeEvent(e); } void KisMainWindow::setActiveView(KisView* view) { d->activeView = view; updateCaption(); if (d->undoActionsUpdateManager) { d->undoActionsUpdateManager->setCurrentDocument(view ? view->document() : 0); } d->viewManager->setCurrentView(view); KisWindowLayoutManager::instance()->activeDocumentChanged(view->document()); } void KisMainWindow::dragMove(QDragMoveEvent * event) { QTabBar *tabBar = d->findTabBarHACK(); if (!tabBar && d->mdiArea->viewMode() == QMdiArea::TabbedView) { qWarning() << "WARNING!!! Cannot find QTabBar in the main window! Looks like Qt has changed behavior. Drag & Drop between multiple tabs might not work properly (tabs will not switch automatically)!"; } if (tabBar && tabBar->isVisible()) { QPoint pos = tabBar->mapFromGlobal(mapToGlobal(event->pos())); if (tabBar->rect().contains(pos)) { const int tabIndex = tabBar->tabAt(pos); if (tabIndex >= 0 && tabBar->currentIndex() != tabIndex) { d->tabSwitchCompressor->start(tabIndex); } } else if (d->tabSwitchCompressor->isActive()) { d->tabSwitchCompressor->stop(); } } } void KisMainWindow::dragLeave() { if (d->tabSwitchCompressor->isActive()) { d->tabSwitchCompressor->stop(); } } void KisMainWindow::switchTab(int index) { QTabBar *tabBar = d->findTabBarHACK(); if (!tabBar) return; tabBar->setCurrentIndex(index); } void KisMainWindow::showWelcomeScreen(bool show) { d->widgetStack->setCurrentIndex(!show); } void KisMainWindow::slotFileNew() { const QStringList mimeFilter = KisImportExportManager::supportedMimeTypes(KisImportExportManager::Import); KisOpenPane *startupWidget = new KisOpenPane(this, mimeFilter, QStringLiteral("templates/")); startupWidget->setWindowModality(Qt::WindowModal); startupWidget->setWindowTitle(i18n("Create new document")); KisConfig cfg(true); int w = cfg.defImageWidth(); int h = cfg.defImageHeight(); const double resolution = cfg.defImageResolution(); const QString colorModel = cfg.defColorModel(); const QString colorDepth = cfg.defaultColorDepth(); const QString colorProfile = cfg.defColorProfile(); CustomDocumentWidgetItem item; item.widget = new KisCustomImageWidget(startupWidget, w, h, resolution, colorModel, colorDepth, colorProfile, i18n("Unnamed")); item.icon = "document-new"; item.title = i18n("Custom Document"); startupWidget->addCustomDocumentWidget(item.widget, item.title, "Custom Document", item.icon); QSize sz = KisClipboard::instance()->clipSize(); if (sz.isValid() && sz.width() != 0 && sz.height() != 0) { w = sz.width(); h = sz.height(); } item.widget = new KisImageFromClipboard(startupWidget, w, h, resolution, colorModel, colorDepth, colorProfile, i18n("Unnamed")); item.title = i18n("Create from Clipboard"); item.icon = "tab-new"; startupWidget->addCustomDocumentWidget(item.widget, item.title, "Create from ClipBoard", item.icon); // calls deleteLater connect(startupWidget, SIGNAL(documentSelected(KisDocument*)), KisPart::instance(), SLOT(startCustomDocument(KisDocument*))); // calls deleteLater connect(startupWidget, SIGNAL(openTemplate(QUrl)), KisPart::instance(), SLOT(openTemplate(QUrl))); startupWidget->exec(); // Cancel calls deleteLater... } void KisMainWindow::slotImportFile() { dbgUI << "slotImportFile()"; slotFileOpen(true); } void KisMainWindow::slotFileOpen(bool isImporting) { #ifndef Q_OS_ANDROID QStringList urls = showOpenFileDialog(isImporting); if (urls.isEmpty()) return; Q_FOREACH (const QString& url, urls) { if (!url.isEmpty()) { OpenFlags flags = isImporting ? Import : None; bool res = openDocument(QUrl::fromLocalFile(url), flags); if (!res) { warnKrita << "Loading" << url << "failed"; } } } #else Q_UNUSED(isImporting) d->fileManager->openImportFile(); connect(d->fileManager, SIGNAL(sigFileSelected(QString)), this, SLOT(slotFileSelected(QString))); connect(d->fileManager, SIGNAL(sigEmptyFilePath()), this, SLOT(slotEmptyFilePath())); #endif } void KisMainWindow::slotFileOpenRecent(const QUrl &url) { (void) openDocument(QUrl::fromLocalFile(url.toLocalFile()), None); } void KisMainWindow::slotFileSave() { if (saveDocument(d->activeView->document(), false, false)) { emit documentSaved(); } } void KisMainWindow::slotFileSaveAs() { if (saveDocument(d->activeView->document(), true, false)) { emit documentSaved(); } } void KisMainWindow::slotExportFile() { if (saveDocument(d->activeView->document(), true, true)) { emit documentSaved(); } } void KisMainWindow::slotShowSessionManager() { KisPart::instance()->showSessionManager(); } KoCanvasResourceProvider *KisMainWindow::resourceManager() const { return d->viewManager->canvasResourceProvider()->resourceManager(); } int KisMainWindow::viewCount() const { return d->mdiArea->subWindowList().size(); } const KConfigGroup &KisMainWindow::windowStateConfig() const { return d->windowStateConfig; } void KisMainWindow::saveWindowState(bool restoreNormalState) { if (restoreNormalState) { QAction *showCanvasOnly = d->viewManager->actionCollection()->action("view_show_canvas_only"); if (showCanvasOnly && showCanvasOnly->isChecked()) { showCanvasOnly->setChecked(false); } d->windowStateConfig.writeEntry("ko_geometry", saveGeometry().toBase64()); d->windowStateConfig.writeEntry("State", saveState().toBase64()); if (!d->dockerStateBeforeHiding.isEmpty()) { restoreState(d->dockerStateBeforeHiding); } statusBar()->setVisible(true); menuBar()->setVisible(true); saveWindowSettings(); } else { saveMainWindowSettings(d->windowStateConfig); } } bool KisMainWindow::restoreWorkspaceState(const QByteArray &state) { QByteArray oldState = saveState(); // needed because otherwise the layout isn't correctly restored in some situations Q_FOREACH (QDockWidget *dock, dockWidgets()) { dock->toggleViewAction()->setEnabled(true); dock->hide(); } bool success = KXmlGuiWindow::restoreState(state); if (!success) { KXmlGuiWindow::restoreState(oldState); return false; } return success; } bool KisMainWindow::restoreWorkspace(KisWorkspaceResource *workspace) { bool success = restoreWorkspaceState(workspace->dockerState()); if (activeKisView()) { activeKisView()->resourceProvider()->notifyLoadingWorkspace(workspace); } return success; } QByteArray KisMainWindow::borrowWorkspace(KisMainWindow *other) { QByteArray currentWorkspace = saveState(); if (!d->workspaceBorrowedBy.isNull()) { if (other->id() == d->workspaceBorrowedBy) { // We're swapping our original workspace back d->workspaceBorrowedBy = QUuid(); return currentWorkspace; } else { // Get our original workspace back before swapping with a third window KisMainWindow *borrower = KisPart::instance()->windowById(d->workspaceBorrowedBy); if (borrower) { QByteArray originalLayout = borrower->borrowWorkspace(this); borrower->restoreWorkspaceState(currentWorkspace); d->workspaceBorrowedBy = other->id(); return originalLayout; } } } d->workspaceBorrowedBy = other->id(); return currentWorkspace; } void KisMainWindow::swapWorkspaces(KisMainWindow *a, KisMainWindow *b) { QByteArray workspaceA = a->borrowWorkspace(b); QByteArray workspaceB = b->borrowWorkspace(a); a->restoreWorkspaceState(workspaceB); b->restoreWorkspaceState(workspaceA); } KisViewManager *KisMainWindow::viewManager() const { return d->viewManager; } void KisMainWindow::slotDocumentInfo() { if (!d->activeView->document()) return; KoDocumentInfo *docInfo = d->activeView->document()->documentInfo(); if (!docInfo) return; KoDocumentInfoDlg *dlg = d->activeView->document()->createDocumentInfoDialog(this, docInfo); if (dlg->exec()) { if (dlg->isDocumentSaved()) { d->activeView->document()->setModified(false); } else { d->activeView->document()->setModified(true); } d->activeView->document()->setTitleModified(); } delete dlg; } bool KisMainWindow::slotFileCloseAll() { Q_FOREACH (QMdiSubWindow *subwin, d->mdiArea->subWindowList()) { if (subwin) { if(!subwin->close()) return false; } } updateCaption(); return true; } void KisMainWindow::slotFileQuit() { // Do not close while KisMainWindow has the savingEntryMutex locked, bug409395. // After the background saving job is initiated, KisDocument blocks closing // while it saves itself. if (hackIsSaving()) { return; } KisPart::instance()->closeSession(); } void KisMainWindow::slotFilePrint() { if (!activeView()) return; KisPrintJob *printJob = activeView()->createPrintJob(); if (printJob == 0) return; applyDefaultSettings(printJob->printer()); QPrintDialog *printDialog = activeView()->createPrintDialog( printJob, this ); if (printDialog && printDialog->exec() == QDialog::Accepted) { printJob->printer().setPageMargins(0.0, 0.0, 0.0, 0.0, QPrinter::Point); printJob->printer().setPaperSize(QSizeF(activeView()->image()->width() / (72.0 * activeView()->image()->xRes()), activeView()->image()->height()/ (72.0 * activeView()->image()->yRes())), QPrinter::Inch); printJob->startPrinting(KisPrintJob::DeleteWhenDone); } else { delete printJob; } delete printDialog; } void KisMainWindow::slotFilePrintPreview() { if (!activeView()) return; KisPrintJob *printJob = activeView()->createPrintJob(); if (printJob == 0) return; /* Sets the startPrinting() slot to be blocking. The Qt print-preview dialog requires the printing to be completely blocking and only return when the full document has been printed. By default the KisPrintingDialog is non-blocking and multithreading, setting blocking to true will allow it to be used in the preview dialog */ printJob->setProperty("blocking", true); QPrintPreviewDialog *preview = new QPrintPreviewDialog(&printJob->printer(), this); printJob->setParent(preview); // will take care of deleting the job connect(preview, SIGNAL(paintRequested(QPrinter*)), printJob, SLOT(startPrinting())); preview->exec(); delete preview; } KisPrintJob* KisMainWindow::exportToPdf(QString pdfFileName) { if (!activeView()) return 0; if (!activeView()->document()) return 0; KoPageLayout pageLayout; pageLayout.width = 0; pageLayout.height = 0; pageLayout.topMargin = 0; pageLayout.bottomMargin = 0; pageLayout.leftMargin = 0; pageLayout.rightMargin = 0; if (pdfFileName.isEmpty()) { KConfigGroup group = KSharedConfig::openConfig()->group("File Dialogs"); QString defaultDir = group.readEntry("SavePdfDialog"); if (defaultDir.isEmpty()) defaultDir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); QUrl startUrl = QUrl::fromLocalFile(defaultDir); KisDocument* pDoc = d->activeView->document(); /** if document has a file name, take file name and replace extension with .pdf */ if (pDoc && pDoc->url().isValid()) { startUrl = pDoc->url(); QString fileName = startUrl.toLocalFile(); fileName = fileName.replace( QRegExp( "\\.\\w{2,5}$", Qt::CaseInsensitive ), ".pdf" ); startUrl = startUrl.adjusted(QUrl::RemoveFilename); startUrl.setPath(startUrl.path() + fileName ); } QPointer layoutDlg(new KoPageLayoutDialog(this, pageLayout)); layoutDlg->setWindowModality(Qt::WindowModal); if (layoutDlg->exec() != QDialog::Accepted || !layoutDlg) { delete layoutDlg; return 0; } pageLayout = layoutDlg->pageLayout(); delete layoutDlg; KoFileDialog dialog(this, KoFileDialog::SaveFile, "OpenDocument"); dialog.setCaption(i18n("Export as PDF")); dialog.setDefaultDir(startUrl.toLocalFile()); dialog.setMimeTypeFilters(QStringList() << "application/pdf"); QUrl url = QUrl::fromUserInput(dialog.filename()); pdfFileName = url.toLocalFile(); if (pdfFileName.isEmpty()) return 0; } KisPrintJob *printJob = activeView()->createPrintJob(); if (printJob == 0) return 0; if (isHidden()) { printJob->setProperty("noprogressdialog", true); } applyDefaultSettings(printJob->printer()); // TODO for remote files we have to first save locally and then upload. printJob->printer().setOutputFileName(pdfFileName); printJob->printer().setDocName(pdfFileName); printJob->printer().setColorMode(QPrinter::Color); if (pageLayout.format == KoPageFormat::CustomSize) { printJob->printer().setPaperSize(QSizeF(pageLayout.width, pageLayout.height), QPrinter::Millimeter); } else { printJob->printer().setPaperSize(KoPageFormat::printerPageSize(pageLayout.format)); } printJob->printer().setPageMargins(pageLayout.leftMargin, pageLayout.topMargin, pageLayout.rightMargin, pageLayout.bottomMargin, QPrinter::Millimeter); switch (pageLayout.orientation) { case KoPageFormat::Portrait: printJob->printer().setOrientation(QPrinter::Portrait); break; case KoPageFormat::Landscape: printJob->printer().setOrientation(QPrinter::Landscape); break; } //before printing check if the printer can handle printing if (!printJob->canPrint()) { QMessageBox::critical(this, i18nc("@title:window", "Krita"), i18n("Cannot export to the specified file")); } printJob->startPrinting(KisPrintJob::DeleteWhenDone); return printJob; } void KisMainWindow::importAnimation() { if (!activeView()) return; KisDocument *document = activeView()->document(); if (!document) return; KisDlgImportImageSequence dlg(this, document); if (dlg.exec() == QDialog::Accepted) { QStringList files = dlg.files(); int firstFrame = dlg.firstFrame(); int step = dlg.step(); KoUpdaterPtr updater = !document->fileBatchMode() ? viewManager()->createUnthreadedUpdater(i18n("Import frames")) : 0; KisAnimationImporter importer(document->image(), updater); KisImportExportErrorCode status = importer.import(files, firstFrame, step); if (!status.isOk() && !status.isInternalError()) { QString msg = status.errorMessage(); if (!msg.isEmpty()) QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("Could not finish import animation:\n%1", msg)); } activeView()->canvasBase()->refetchDataFromImage(); } } void KisMainWindow::slotConfigureToolbars() { saveWindowState(); KEditToolBar edit(factory(), this); connect(&edit, SIGNAL(newToolBarConfig()), this, SLOT(slotNewToolbarConfig())); (void) edit.exec(); applyToolBarLayout(); } void KisMainWindow::slotNewToolbarConfig() { applyMainWindowSettings(d->windowStateConfig); KXMLGUIFactory *factory = guiFactory(); Q_UNUSED(factory); // Check if there's an active view if (!d->activeView) return; plugActionList("toolbarlist", d->toolbarList); applyToolBarLayout(); } void KisMainWindow::slotToolbarToggled(bool toggle) { //dbgUI <<"KisMainWindow::slotToolbarToggled" << sender()->name() <<" toggle=" << true; // The action (sender) and the toolbar have the same name KToolBar * bar = toolBar(sender()->objectName()); if (bar) { if (toggle) { bar->show(); } else { bar->hide(); } if (d->activeView && d->activeView->document()) { saveWindowState(); } } else warnUI << "slotToolbarToggled : Toolbar " << sender()->objectName() << " not found!"; } void KisMainWindow::viewFullscreen(bool fullScreen) { KisConfig cfg(false); cfg.setFullscreenMode(fullScreen); if (fullScreen) { setWindowState(windowState() | Qt::WindowFullScreen); // set } else { setWindowState(windowState() & ~Qt::WindowFullScreen); // reset } } void KisMainWindow::setMaxRecentItems(uint _number) { d->recentFiles->setMaxItems(_number); } void KisMainWindow::slotReloadFile() { KisDocument* document = d->activeView->document(); if (!document || document->url().isEmpty()) return; if (document->isModified()) { bool ok = QMessageBox::question(this, i18nc("@title:window", "Krita"), i18n("You will lose all changes made since your last save\n" "Do you want to continue?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) == QMessageBox::Yes; if (!ok) return; } QUrl url = document->url(); saveWindowSettings(); if (!document->reload()) { QMessageBox::critical(this, i18nc("@title:window", "Krita"), i18n("Error: Could not reload this document")); } return; } QDockWidget* KisMainWindow::createDockWidget(KoDockFactoryBase* factory) { QDockWidget* dockWidget = 0; bool lockAllDockers = KisConfig(true).readEntry("LockAllDockerPanels", false); if (!d->dockWidgetsMap.contains(factory->id())) { dockWidget = factory->createDockWidget(); // It is quite possible that a dock factory cannot create the dock; don't // do anything in that case. if (!dockWidget) { warnKrita << "Could not create docker for" << factory->id(); return 0; } dockWidget->setFont(KoDockRegistry::dockFont()); dockWidget->setObjectName(factory->id()); dockWidget->setParent(this); if (lockAllDockers) { if (dockWidget->titleBarWidget()) { dockWidget->titleBarWidget()->setVisible(false); } dockWidget->setFeatures(QDockWidget::NoDockWidgetFeatures); } if (dockWidget->widget() && dockWidget->widget()->layout()) dockWidget->widget()->layout()->setContentsMargins(1, 1, 1, 1); Qt::DockWidgetArea side = Qt::RightDockWidgetArea; bool visible = true; switch (factory->defaultDockPosition()) { case KoDockFactoryBase::DockTornOff: dockWidget->setFloating(true); // position nicely? break; case KoDockFactoryBase::DockTop: side = Qt::TopDockWidgetArea; break; case KoDockFactoryBase::DockLeft: side = Qt::LeftDockWidgetArea; break; case KoDockFactoryBase::DockBottom: side = Qt::BottomDockWidgetArea; break; case KoDockFactoryBase::DockRight: side = Qt::RightDockWidgetArea; break; case KoDockFactoryBase::DockMinimized: default: side = Qt::RightDockWidgetArea; visible = false; } KConfigGroup group = d->windowStateConfig.group("DockWidget " + factory->id()); side = static_cast(group.readEntry("DockArea", static_cast(side))); if (side == Qt::NoDockWidgetArea) side = Qt::RightDockWidgetArea; addDockWidget(side, dockWidget); if (!visible) { dockWidget->hide(); } d->dockWidgetsMap.insert(factory->id(), dockWidget); } else { dockWidget = d->dockWidgetsMap[factory->id()]; } #ifdef Q_OS_MACOS dockWidget->setAttribute(Qt::WA_MacSmallSize, true); #endif dockWidget->setFont(KoDockRegistry::dockFont()); connect(dockWidget, SIGNAL(dockLocationChanged(Qt::DockWidgetArea)), this, SLOT(forceDockTabFonts())); return dockWidget; } void KisMainWindow::forceDockTabFonts() { Q_FOREACH (QObject *child, children()) { if (child->inherits("QTabBar")) { ((QTabBar *)child)->setFont(KoDockRegistry::dockFont()); } } } QList KisMainWindow::dockWidgets() const { return d->dockWidgetsMap.values(); } QDockWidget* KisMainWindow::dockWidget(const QString &id) { if (!d->dockWidgetsMap.contains(id)) return 0; return d->dockWidgetsMap[id]; } QList KisMainWindow::canvasObservers() const { QList observers; Q_FOREACH (QDockWidget *docker, dockWidgets()) { KoCanvasObserverBase *observer = dynamic_cast(docker); if (observer) { observers << observer; } else { warnKrita << docker << "is not a canvas observer"; } } return observers; } void KisMainWindow::toggleDockersVisibility(bool visible) { if (!visible) { d->dockerStateBeforeHiding = saveState(); Q_FOREACH (QObject* widget, children()) { if (widget->inherits("QDockWidget")) { QDockWidget* dw = static_cast(widget); if (dw->isVisible()) { dw->hide(); } } } } else { restoreState(d->dockerStateBeforeHiding); } } void KisMainWindow::slotDocumentTitleModified() { updateCaption(); updateReloadFileAction(d->activeView ? d->activeView->document() : 0); } void KisMainWindow::subWindowActivated() { bool enabled = (activeKisView() != 0); d->mdiCascade->setEnabled(enabled); d->mdiNextWindow->setEnabled(enabled); d->mdiPreviousWindow->setEnabled(enabled); d->mdiTile->setEnabled(enabled); d->close->setEnabled(enabled); d->closeAll->setEnabled(enabled); setActiveSubWindow(d->mdiArea->activeSubWindow()); Q_FOREACH (QToolBar *tb, toolBars()) { if (tb->objectName() == "BrushesAndStuff") { tb->setEnabled(enabled); } } /** * Qt has a weirdness, it has hardcoded shortcuts added to an action * in the window menu. We need to reset the shortcuts for that menu * to nothing, otherwise the shortcuts cannot be made configurable. * * See: https://bugs.kde.org/show_bug.cgi?id=352205 * https://bugs.kde.org/show_bug.cgi?id=375524 * https://bugs.kde.org/show_bug.cgi?id=398729 */ QMdiSubWindow *subWindow = d->mdiArea->currentSubWindow(); if (subWindow) { QMenu *menu = subWindow->systemMenu(); if (menu && menu->actions().size() == 8) { Q_FOREACH (QAction *action, menu->actions()) { action->setShortcut(QKeySequence()); } menu->actions().last()->deleteLater(); } } updateCaption(); d->actionManager()->updateGUI(); } void KisMainWindow::windowFocused() { /** * Notify selection manager so that it could update selection mask overlay */ if (viewManager() && viewManager()->selectionManager()) { viewManager()->selectionManager()->selectionChanged(); } KisPart *kisPart = KisPart::instance(); KisWindowLayoutManager *layoutManager = KisWindowLayoutManager::instance(); if (!layoutManager->primaryWorkspaceFollowsFocus()) return; QUuid primary = layoutManager->primaryWindowId(); if (primary.isNull()) return; if (d->id == primary) { if (!d->workspaceBorrowedBy.isNull()) { KisMainWindow *borrower = kisPart->windowById(d->workspaceBorrowedBy); if (!borrower) return; swapWorkspaces(this, borrower); } } else { if (d->workspaceBorrowedBy == primary) return; KisMainWindow *primaryWindow = kisPart->windowById(primary); if (!primaryWindow) return; swapWorkspaces(this, primaryWindow); } } void KisMainWindow::updateWindowMenu() { QMenu *menu = d->windowMenu->menu(); menu->clear(); menu->addAction(d->newWindow); menu->addAction(d->documentMenu); QMenu *docMenu = d->documentMenu->menu(); docMenu->clear(); QFontMetrics fontMetrics = docMenu->fontMetrics(); int fileStringWidth = int(QApplication::desktop()->screenGeometry(this).width() * .40f); Q_FOREACH (QPointer doc, KisPart::instance()->documents()) { if (doc) { QString title = fontMetrics.elidedText(doc->url().toDisplayString(QUrl::PreferLocalFile), Qt::ElideMiddle, fileStringWidth); if (title.isEmpty() && doc->image()) { title = doc->image()->objectName(); } QAction *action = docMenu->addAction(title); action->setIcon(qApp->windowIcon()); connect(action, SIGNAL(triggered()), d->documentMapper, SLOT(map())); d->documentMapper->setMapping(action, doc); } } menu->addAction(d->workspaceMenu); QMenu *workspaceMenu = d->workspaceMenu->menu(); workspaceMenu->clear(); auto workspaces = KisResourceServerProvider::instance()->workspaceServer()->resources(); auto m_this = this; for (auto &w : workspaces) { auto action = workspaceMenu->addAction(w->name()); connect(action, &QAction::triggered, this, [=]() { m_this->restoreWorkspace(w); }); } workspaceMenu->addSeparator(); connect(workspaceMenu->addAction(i18nc("@action:inmenu", "&Import Workspace...")), &QAction::triggered, this, [&]() { QString extensions = d->workspacemodel->extensions(); QStringList mimeTypes; for(const QString &suffix : extensions.split(":")) { mimeTypes << KisMimeDatabase::mimeTypeForSuffix(suffix); } KoFileDialog dialog(0, KoFileDialog::OpenFile, "OpenDocument"); dialog.setMimeTypeFilters(mimeTypes); dialog.setCaption(i18nc("@title:window", "Choose File to Add")); QString filename = dialog.filename(); d->workspacemodel->importResourceFile(filename); }); connect(workspaceMenu->addAction(i18nc("@action:inmenu", "&New Workspace...")), &QAction::triggered, [=]() { QString name = QInputDialog::getText(this, i18nc("@title:window", "New Workspace..."), i18nc("@label:textbox", "Name:")); if (name.isEmpty()) return; auto rserver = KisResourceServerProvider::instance()->workspaceServer(); KisWorkspaceResource* workspace = new KisWorkspaceResource(""); workspace->setDockerState(m_this->saveState()); d->viewManager->canvasResourceProvider()->notifySavingWorkspace(workspace); workspace->setValid(true); QString saveLocation = rserver->saveLocation(); bool newName = false; if(name.isEmpty()) { newName = true; name = i18n("Workspace"); } QFileInfo fileInfo(saveLocation + name + workspace->defaultFileExtension()); int i = 1; while (fileInfo.exists()) { fileInfo.setFile(saveLocation + name + QString("%1").arg(i) + workspace->defaultFileExtension()); i++; } workspace->setFilename(fileInfo.filePath()); if(newName) { name = i18n("Workspace %1", i); } workspace->setName(name); rserver->addResource(workspace); }); // TODO: What to do about delete? // workspaceMenu->addAction(i18nc("@action:inmenu", "&Delete Workspace...")); menu->addSeparator(); menu->addAction(d->close); menu->addAction(d->closeAll); if (d->mdiArea->viewMode() == QMdiArea::SubWindowView) { menu->addSeparator(); menu->addAction(d->mdiTile); menu->addAction(d->mdiCascade); } menu->addSeparator(); menu->addAction(d->mdiNextWindow); menu->addAction(d->mdiPreviousWindow); menu->addSeparator(); QList windows = d->mdiArea->subWindowList(); for (int i = 0; i < windows.size(); ++i) { QPointerchild = qobject_cast(windows.at(i)->widget()); if (child && child->document()) { QString text; if (i < 9) { text = i18n("&%1 %2", i + 1, fontMetrics.elidedText(child->document()->url().toDisplayString(QUrl::PreferLocalFile), Qt::ElideMiddle, fileStringWidth)); } else { text = i18n("%1 %2", i + 1, fontMetrics.elidedText(child->document()->url().toDisplayString(QUrl::PreferLocalFile), Qt::ElideMiddle, fileStringWidth)); } QAction *action = menu->addAction(text); action->setIcon(qApp->windowIcon()); action->setCheckable(true); action->setChecked(child == activeKisView()); connect(action, SIGNAL(triggered()), d->windowMapper, SLOT(map())); d->windowMapper->setMapping(action, windows.at(i)); } } bool showMdiArea = windows.count( ) > 0; if (!showMdiArea) { showWelcomeScreen(true); // see workaround in function in header // keep the recent file list updated when going back to welcome screen reloadRecentFileList(); d->welcomePage->populateRecentDocuments(); } // enable/disable the toolbox docker if there are no documents open Q_FOREACH (QObject* widget, children()) { if (widget->inherits("QDockWidget")) { QDockWidget* dw = static_cast(widget); if ( dw->objectName() == "ToolBox") { dw->setEnabled(showMdiArea); } } } updateCaption(); } void KisMainWindow::setActiveSubWindow(QWidget *window) { if (!window) return; QMdiSubWindow *subwin = qobject_cast(window); //dbgKrita << "setActiveSubWindow();" << subwin << d->activeSubWindow; if (subwin && subwin != d->activeSubWindow) { KisView *view = qobject_cast(subwin->widget()); //dbgKrita << "\t" << view << activeView(); if (view && view != activeView()) { d->mdiArea->setActiveSubWindow(subwin); setActiveView(view); } d->activeSubWindow = subwin; } updateWindowMenu(); d->actionManager()->updateGUI(); } void KisMainWindow::configChanged() { KisConfig cfg(true); QMdiArea::ViewMode viewMode = (QMdiArea::ViewMode)cfg.readEntry("mdi_viewmode", (int)QMdiArea::TabbedView); d->mdiArea->setViewMode(viewMode); Q_FOREACH (QMdiSubWindow *subwin, d->mdiArea->subWindowList()) { subwin->setOption(QMdiSubWindow::RubberBandMove, cfg.readEntry("mdi_rubberband", cfg.useOpenGL())); subwin->setOption(QMdiSubWindow::RubberBandResize, cfg.readEntry("mdi_rubberband", cfg.useOpenGL())); /** * Dirty workaround for a bug in Qt (checked on Qt 5.6.1): * * If you make a window "Show on top" and then switch to the tabbed mode - * the window will contiue to be painted in its initial "mid-screen" + * the window will continue to be painted in its initial "mid-screen" * position. It will persist here until you explicitly switch to its tab. */ if (viewMode == QMdiArea::TabbedView) { Qt::WindowFlags oldFlags = subwin->windowFlags(); Qt::WindowFlags flags = oldFlags; flags &= ~Qt::WindowStaysOnTopHint; flags &= ~Qt::WindowStaysOnBottomHint; if (flags != oldFlags) { subwin->setWindowFlags(flags); subwin->showMaximized(); } } } KConfigGroup group( KSharedConfig::openConfig(), "theme"); d->themeManager->setCurrentTheme(group.readEntry("Theme", "Krita dark")); d->actionManager()->updateGUI(); QString s = cfg.getMDIBackgroundColor(); KoColor c = KoColor::fromXML(s); QBrush brush(c.toQColor()); d->mdiArea->setBackground(brush); QString backgroundImage = cfg.getMDIBackgroundImage(); if (backgroundImage != "") { QImage image(backgroundImage); QBrush brush(image); d->mdiArea->setBackground(brush); } d->mdiArea->update(); } KisView* KisMainWindow::newView(QObject *document, QMdiSubWindow *subWindow) { KisDocument *doc = qobject_cast(document); KisView *view = addViewAndNotifyLoadingCompleted(doc, subWindow); d->actionManager()->updateGUI(); return view; } void KisMainWindow::newWindow() { KisMainWindow *mainWindow = KisPart::instance()->createMainWindow(); mainWindow->initializeGeometry(); mainWindow->show(); } void KisMainWindow::closeCurrentWindow() { if (d->mdiArea->currentSubWindow()) { d->mdiArea->currentSubWindow()->close(); d->actionManager()->updateGUI(); } } void KisMainWindow::checkSanity() { // print error if the lcms engine is not available if (!KoColorSpaceEngineRegistry::instance()->contains("icc")) { // need to wait 1 event since exiting here would not work. m_errorMessage = i18n("The Krita LittleCMS color management plugin is not installed. Krita will quit now."); m_dieOnError = true; QTimer::singleShot(0, this, SLOT(showErrorAndDie())); return; } KisPaintOpPresetResourceServer * rserver = KisResourceServerProvider::instance()->paintOpPresetServer(); if (rserver->resources().isEmpty()) { m_errorMessage = i18n("Krita cannot find any brush presets! Krita will quit now."); m_dieOnError = true; QTimer::singleShot(0, this, SLOT(showErrorAndDie())); return; } } void KisMainWindow::showErrorAndDie() { QMessageBox::critical(0, i18nc("@title:window", "Installation error"), m_errorMessage); if (m_dieOnError) { exit(10); } } void KisMainWindow::showAboutApplication() { KisAboutApplication dlg(this); dlg.exec(); } QPointer KisMainWindow::activeKisView() { if (!d->mdiArea) return 0; QMdiSubWindow *activeSubWindow = d->mdiArea->activeSubWindow(); //dbgKrita << "activeKisView" << activeSubWindow; if (!activeSubWindow) return 0; return qobject_cast(activeSubWindow->widget()); } void KisMainWindow::newOptionWidgets(KoCanvasController *controller, const QList > &optionWidgetList) { KIS_ASSERT_RECOVER_NOOP(controller == KoToolManager::instance()->activeCanvasController()); bool isOurOwnView = false; Q_FOREACH (QPointer view, KisPart::instance()->views()) { if (view && view->canvasController() == controller) { isOurOwnView = view->mainWindow() == this; } } if (!isOurOwnView) return; Q_FOREACH (QWidget *w, optionWidgetList) { #ifdef Q_OS_MACOS w->setAttribute(Qt::WA_MacSmallSize, true); #endif w->setFont(KoDockRegistry::dockFont()); } if (d->toolOptionsDocker) { d->toolOptionsDocker->setOptionWidgets(optionWidgetList); } else { d->viewManager->paintOpBox()->newOptionWidgets(optionWidgetList); } } void KisMainWindow::applyDefaultSettings(QPrinter &printer) { if (!d->activeView) return; QString title = d->activeView->document()->documentInfo()->aboutInfo("title"); if (title.isEmpty()) { QFileInfo info(d->activeView->document()->url().fileName()); title = info.completeBaseName(); } if (title.isEmpty()) { // #139905 title = i18n("%1 unsaved document (%2)", qApp->applicationDisplayName(), QLocale().toString(QDate::currentDate(), QLocale::ShortFormat)); } printer.setDocName(title); } void KisMainWindow::createActions() { KisActionManager *actionManager = d->actionManager(); actionManager->createStandardAction(KStandardAction::New, this, SLOT(slotFileNew())); actionManager->createStandardAction(KStandardAction::Open, this, SLOT(slotFileOpen())); actionManager->createStandardAction(KStandardAction::Quit, this, SLOT(slotFileQuit())); actionManager->createStandardAction(KStandardAction::ConfigureToolbars, this, SLOT(slotConfigureToolbars())); d->fullScreenMode = actionManager->createStandardAction(KStandardAction::FullScreen, this, SLOT(viewFullscreen(bool))); d->recentFiles = KStandardAction::openRecent(this, SLOT(slotFileOpenRecent(QUrl)), actionCollection()); connect(d->recentFiles, SIGNAL(recentListCleared()), this, SLOT(saveRecentFiles())); KSharedConfigPtr configPtr = KSharedConfig::openConfig(); d->recentFiles->loadEntries(configPtr->group("RecentFiles")); d->saveAction = actionManager->createStandardAction(KStandardAction::Save, this, SLOT(slotFileSave())); d->saveAction->setActivationFlags(KisAction::ACTIVE_IMAGE); d->saveActionAs = actionManager->createStandardAction(KStandardAction::SaveAs, this, SLOT(slotFileSaveAs())); d->saveActionAs->setActivationFlags(KisAction::ACTIVE_IMAGE); // d->printAction = actionManager->createStandardAction(KStandardAction::Print, this, SLOT(slotFilePrint())); // d->printAction->setActivationFlags(KisAction::ACTIVE_IMAGE); // d->printActionPreview = actionManager->createStandardAction(KStandardAction::PrintPreview, this, SLOT(slotFilePrintPreview())); // d->printActionPreview->setActivationFlags(KisAction::ACTIVE_IMAGE); d->undo = actionManager->createStandardAction(KStandardAction::Undo, this, SLOT(undo())); d->undo->setActivationFlags(KisAction::ACTIVE_IMAGE); d->redo = actionManager->createStandardAction(KStandardAction::Redo, this, SLOT(redo())); d->redo->setActivationFlags(KisAction::ACTIVE_IMAGE); d->undoActionsUpdateManager.reset(new KisUndoActionsUpdateManager(d->undo, d->redo)); d->undoActionsUpdateManager->setCurrentDocument(d->activeView ? d->activeView->document() : 0); // d->exportPdf = actionManager->createAction("file_export_pdf"); // connect(d->exportPdf, SIGNAL(triggered()), this, SLOT(exportToPdf())); d->importAnimation = actionManager->createAction("file_import_animation"); connect(d->importAnimation, SIGNAL(triggered()), this, SLOT(importAnimation())); d->closeAll = actionManager->createAction("file_close_all"); connect(d->closeAll, SIGNAL(triggered()), this, SLOT(slotFileCloseAll())); // d->reloadFile = actionManager->createAction("file_reload_file"); // d->reloadFile->setActivationFlags(KisAction::CURRENT_IMAGE_MODIFIED); // connect(d->reloadFile, SIGNAL(triggered(bool)), this, SLOT(slotReloadFile())); d->importFile = actionManager->createAction("file_import_file"); connect(d->importFile, SIGNAL(triggered(bool)), this, SLOT(slotImportFile())); d->exportFile = actionManager->createAction("file_export_file"); connect(d->exportFile, SIGNAL(triggered(bool)), this, SLOT(slotExportFile())); /* The following entry opens the document information dialog. Since the action is named so it intends to show data this entry should not have a trailing ellipses (...). */ d->showDocumentInfo = actionManager->createAction("file_documentinfo"); connect(d->showDocumentInfo, SIGNAL(triggered(bool)), this, SLOT(slotDocumentInfo())); d->themeManager->setThemeMenuAction(new KActionMenu(i18nc("@action:inmenu", "&Themes"), this)); d->themeManager->registerThemeActions(actionCollection()); connect(d->themeManager, SIGNAL(signalThemeChanged()), this, SLOT(slotThemeChanged())); connect(d->themeManager, SIGNAL(signalThemeChanged()), d->welcomePage, SLOT(slotUpdateThemeColors())); d->toggleDockers = actionManager->createAction("view_toggledockers"); KisConfig(true).showDockers(true); d->toggleDockers->setChecked(true); connect(d->toggleDockers, SIGNAL(toggled(bool)), SLOT(toggleDockersVisibility(bool))); d->toggleDetachCanvas = actionManager->createAction("view_detached_canvas"); d->toggleDetachCanvas->setChecked(false); connect(d->toggleDetachCanvas, SIGNAL(toggled(bool)), SLOT(setCanvasDetached(bool))); setCanvasDetached(false); actionCollection()->addAction("settings_dockers_menu", d->dockWidgetMenu); actionCollection()->addAction("window", d->windowMenu); d->mdiCascade = actionManager->createAction("windows_cascade"); connect(d->mdiCascade, SIGNAL(triggered()), d->mdiArea, SLOT(cascadeSubWindows())); d->mdiTile = actionManager->createAction("windows_tile"); connect(d->mdiTile, SIGNAL(triggered()), d->mdiArea, SLOT(tileSubWindows())); d->mdiNextWindow = actionManager->createAction("windows_next"); connect(d->mdiNextWindow, SIGNAL(triggered()), d->mdiArea, SLOT(activateNextSubWindow())); d->mdiPreviousWindow = actionManager->createAction("windows_previous"); connect(d->mdiPreviousWindow, SIGNAL(triggered()), d->mdiArea, SLOT(activatePreviousSubWindow())); d->newWindow = actionManager->createAction("view_newwindow"); connect(d->newWindow, SIGNAL(triggered(bool)), this, SLOT(newWindow())); d->close = actionManager->createStandardAction(KStandardAction::Close, this, SLOT(closeCurrentWindow())); d->showSessionManager = actionManager->createAction("file_sessions"); connect(d->showSessionManager, SIGNAL(triggered(bool)), this, SLOT(slotShowSessionManager())); actionManager->createStandardAction(KStandardAction::Preferences, this, SLOT(slotPreferences())); for (int i = 0; i < 2; i++) { d->expandingSpacers[i] = new KisAction(i18n("Expanding Spacer")); d->expandingSpacers[i]->setDefaultWidget(new QWidget(this)); d->expandingSpacers[i]->defaultWidget()->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); actionManager->addAction(QString("expanding_spacer_%1").arg(i), d->expandingSpacers[i]); } } void KisMainWindow::applyToolBarLayout() { const bool isPlastiqueStyle = style()->objectName() == "plastique"; Q_FOREACH (KToolBar *toolBar, toolBars()) { toolBar->layout()->setSpacing(4); if (isPlastiqueStyle) { toolBar->setContentsMargins(0, 0, 0, 2); } //Hide text for buttons with an icon in the toolbar Q_FOREACH (QAction *ac, toolBar->actions()){ if (ac->icon().pixmap(QSize(1,1)).isNull() == false){ ac->setPriority(QAction::LowPriority); }else { ac->setIcon(QIcon()); } } } } void KisMainWindow::initializeGeometry() { // if the user didn's specify the geometry on the command line (does anyone do that still?), // we first figure out some good default size and restore the x,y position. See bug 285804Z. KConfigGroup cfg = d->windowStateConfig; QByteArray geom = QByteArray::fromBase64(cfg.readEntry("ko_geometry", QByteArray())); if (!restoreGeometry(geom)) { const int scnum = QApplication::desktop()->screenNumber(parentWidget()); QRect desk = QGuiApplication::screens().at(scnum)->availableVirtualGeometry(); quint32 x = desk.x(); quint32 y = desk.y(); quint32 w = 0; quint32 h = 0; // Default size -- maximize on small screens, something useful on big screens const int deskWidth = desk.width(); if (deskWidth > 1024) { // a nice width, and slightly less than total available // height to compensate for the window decs w = (deskWidth / 3) * 2; h = (desk.height() / 3) * 2; } else { w = desk.width(); h = desk.height(); } x += (desk.width() - w) / 2; y += (desk.height() - h) / 2; move(x,y); setGeometry(geometry().x(), geometry().y(), w, h); } d->fullScreenMode->setChecked(isFullScreen()); } void KisMainWindow::showManual() { QDesktopServices::openUrl(QUrl("https://docs.krita.org")); } void KisMainWindow::windowScreenChanged(QScreen *screen) { emit screenChanged(); d->screenConnectionsStore.clear(); d->screenConnectionsStore.addConnection(screen, SIGNAL(physicalDotsPerInchChanged(qreal)), this, SIGNAL(screenChanged())); } void KisMainWindow::slotXmlGuiMakingChanges(bool finished) { if (finished) { subWindowActivated(); } } #include diff --git a/libs/ui/KisPaletteEditor.h b/libs/ui/KisPaletteEditor.h index a073381f1c..b11c05a977 100644 --- a/libs/ui/KisPaletteEditor.h +++ b/libs/ui/KisPaletteEditor.h @@ -1,146 +1,146 @@ /* * Copyright (c) 2018 Michael Zhou * * 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 KISPALETTEMANAGER_H #define KISPALETTEMANAGER_H #include #include #include #include class KoColorSet; class KisPaletteModel; class KisViewManager; class KisSwatchGroup; class KisViewManager; /** * @brief The PaletteEditor class * this class manipulates a KisPaletteModel using GUI elements and communicate * with KisDocument * * Changes made in this class won't be done to the palette if the palette is * read only (not editable, isEditable() == false) */ class KRITAUI_EXPORT KisPaletteEditor : public QObject { Q_OBJECT public: struct PaletteInfo; public: explicit KisPaletteEditor(QObject *parent = 0); ~KisPaletteEditor(); void setPaletteModel(KisPaletteModel *model); void setView(KisViewManager *view); void addPalette(); void importPalette(); void removePalette(KoColorSet *); /** * @brief rowNumberOfGroup * @param oriName the original name of a group at the creation of the instance * @return newest row number of the group */ int rowNumberOfGroup(const QString &oriName) const; /** * @brief oldNameFromNewName * @param newName the current name of a group * @return the name of the group at the creation of the instance */ QString oldNameFromNewName(const QString &newName) const; /** * @brief duplicateExistsFilename * @param filename the name of the file * @param global if this filename is going to be used for a global palette * @return true if the a palette in the resource system that has filename * name already exists else false */ bool duplicateExistsFilename(const QString &filename, bool global) const; QString relativePathFromSaveLocation() const; void rename(const QString &newName); void changeFilename(const QString &newName); void changeColCount(int); /** * @brief addGroup * @return new group's name if change accepted, empty string if cancelled */ QString addGroup(); /** * @brief removeGroup * @param name original group name * @return true if change accepted, false if cancelled */ bool removeGroup(const QString &name); /** * @brief renameGroup * @param oldName - * @return new name if change accpeted, empty string if cancelled + * @return new name if change accepted, empty string if cancelled */ QString renameGroup(const QString &oldName); void changeGroupRowCount(const QString &name, int newRowCount); void setGlobal(bool); void setReadOnly(bool); void setEntry(const KoColor &color, const QModelIndex &index); void removeEntry(const QModelIndex &index); void modifyEntry(const QModelIndex &index); void addEntry(const KoColor &color); bool isModified() const; /** * @brief getModifiedGroup * @param originalName name of the group at the creation of the instance * @return the modified group */ const KisSwatchGroup &getModifiedGroup(const QString &originalName) const; /** * @brief updatePalette * MUST be called to make the changes into the resource server */ void updatePalette(); private Q_SLOTS: void slotGroupNameChanged(const QString &newName); void slotPaletteChanged(); void slotSetDocumentModified(); private: QString newPaletteFileName(bool isGlobal, const QString &filename = QString()); QString newGroupName() const; void setNonGlobal(); void setGlobal(); bool duplicateExistsGroupName(const QString &name) const; bool duplicateExistsOriginalGroupName(const QString &name) const; void uploadPaletteList() const; QString filenameFromPath(const QString &path) const; private: struct Private; QScopedPointer m_d; }; #endif // KISPALETTEMANAGER_H diff --git a/libs/ui/canvas/kis_canvas2.cpp b/libs/ui/canvas/kis_canvas2.cpp index 5d87d911d3..e68f48b78d 100644 --- a/libs/ui/canvas/kis_canvas2.cpp +++ b/libs/ui/canvas/kis_canvas2.cpp @@ -1,1280 +1,1280 @@ /* This file is part of the KDE project * * Copyright (C) 2006, 2010 Boudewijn Rempt * Copyright (C) Lukáš Tvrdý , (C) 2010 * 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_canvas2.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_tool_proxy.h" #include "kis_coordinates_converter.h" #include "kis_prescaled_projection.h" #include "kis_image.h" #include "kis_image_barrier_locker.h" #include "kis_undo_adapter.h" #include "flake/kis_shape_layer.h" #include "kis_canvas_resource_provider.h" #include "KisViewManager.h" #include "kis_config.h" #include "kis_config_notifier.h" #include "kis_abstract_canvas_widget.h" #include "kis_qpainter_canvas.h" #include "kis_group_layer.h" #include "flake/kis_shape_controller.h" #include "kis_node_manager.h" #include "kis_selection.h" #include "kis_selection_component.h" #include "flake/kis_shape_selection.h" #include "kis_selection_mask.h" #include "kis_image_config.h" #include "kis_infinity_manager.h" #include "kis_signal_compressor.h" #include "kis_display_color_converter.h" #include "kis_exposure_gamma_correction_interface.h" #include "KisView.h" #include "kis_canvas_controller.h" #include "kis_grid_config.h" #include "kis_animation_player.h" #include "kis_animation_frame_cache.h" #include "opengl/kis_opengl_canvas2.h" #include "opengl/kis_opengl.h" #include "kis_fps_decoration.h" #include "KoColorConversionTransformation.h" #include "KisProofingConfiguration.h" #include #include #include "input/kis_input_manager.h" #include "kis_painting_assistants_decoration.h" #include "kis_canvas_updates_compressor.h" #include "KoZoomController.h" #include #include "opengl/kis_opengl_canvas_debugger.h" #include "kis_algebra_2d.h" #include "kis_image_signal_router.h" #include "KisSnapPixelStrategy.h" class Q_DECL_HIDDEN KisCanvas2::KisCanvas2Private { public: KisCanvas2Private(KoCanvasBase *parent, KisCoordinatesConverter* coordConverter, QPointer view, KoCanvasResourceProvider* resourceManager) : coordinatesConverter(coordConverter) , view(view) , shapeManager(parent) , selectedShapesProxy(&shapeManager) , toolProxy(parent) , displayColorConverter(resourceManager, view) , regionOfInterestUpdateCompressor(100, KisSignalCompressor::FIRST_INACTIVE) { } KisCoordinatesConverter *coordinatesConverter; QPointerview; KisAbstractCanvasWidget *canvasWidget = 0; KoShapeManager shapeManager; KisSelectedShapesProxy selectedShapesProxy; bool currentCanvasIsOpenGL; int openGLFilterMode; KisToolProxy toolProxy; KisPrescaledProjectionSP prescaledProjection; bool vastScrolling; KisSignalCompressor canvasUpdateCompressor; QRect savedUpdateRect; QBitArray channelFlags; KisProofingConfigurationSP proofingConfig; bool softProofing = false; bool gamutCheck = false; bool proofingConfigUpdated = false; KisPopupPalette *popupPalette = 0; KisDisplayColorConverter displayColorConverter; KisCanvasUpdatesCompressor projectionUpdatesCompressor; KisAnimationPlayer *animationPlayer; KisAnimationFrameCacheSP frameCache; bool lodAllowedInImage = false; bool bootstrapLodBlocked; QPointer currentlyActiveShapeManager; KisInputActionGroupsMask inputActionGroupsMask = AllActionGroup; KisSignalCompressor frameRenderStartCompressor; KisSignalCompressor regionOfInterestUpdateCompressor; QRect regionOfInterest; qreal regionOfInterestMargin = 0.25; QRect renderingLimit; int isBatchUpdateActive = 0; bool effectiveLodAllowedInImage() { return lodAllowedInImage && !bootstrapLodBlocked; } void setActiveShapeManager(KoShapeManager *shapeManager); }; namespace { KoShapeManager* fetchShapeManagerFromNode(KisNodeSP node) { KoShapeManager *shapeManager = 0; KisSelectionSP selection; if (KisLayer *layer = dynamic_cast(node.data())) { KisShapeLayer *shapeLayer = dynamic_cast(layer); if (shapeLayer) { shapeManager = shapeLayer->shapeManager(); } } else if (KisSelectionMask *mask = dynamic_cast(node.data())) { selection = mask->selection(); } if (!shapeManager && selection && selection->hasShapeSelection()) { KisShapeSelection *shapeSelection = dynamic_cast(selection->shapeSelection()); KIS_ASSERT_RECOVER_RETURN_VALUE(shapeSelection, 0); shapeManager = shapeSelection->shapeManager(); } return shapeManager; } } KisCanvas2::KisCanvas2(KisCoordinatesConverter *coordConverter, KoCanvasResourceProvider *resourceManager, KisMainWindow *mainWindow, KisView *view, KoShapeControllerBase *sc) : KoCanvasBase(sc, resourceManager) , m_d(new KisCanvas2Private(this, coordConverter, view, resourceManager)) { /** * While loading LoD should be blocked. Only when GUI has finished * loading and zoom level settled down, LoD is given a green * light. */ m_d->bootstrapLodBlocked = true; connect(mainWindow, SIGNAL(guiLoadingFinished()), SLOT(bootstrapFinished())); connect(mainWindow, SIGNAL(screenChanged()), SLOT(slotConfigChanged())); KisImageConfig config(false); m_d->canvasUpdateCompressor.setDelay(1000 / config.fpsLimit()); m_d->canvasUpdateCompressor.setMode(KisSignalCompressor::FIRST_ACTIVE); m_d->frameRenderStartCompressor.setDelay(1000 / config.fpsLimit()); m_d->frameRenderStartCompressor.setMode(KisSignalCompressor::FIRST_ACTIVE); snapGuide()->overrideSnapStrategy(KoSnapGuide::PixelSnapping, new KisSnapPixelStrategy()); } void KisCanvas2::setup() { // a bit of duplication from slotConfigChanged() KisConfig cfg(true); m_d->vastScrolling = cfg.vastScrolling(); m_d->lodAllowedInImage = cfg.levelOfDetailEnabled(); m_d->regionOfInterestMargin = KisImageConfig(true).animationCacheRegionOfInterestMargin(); createCanvas(cfg.useOpenGL()); setLodAllowedInCanvas(m_d->lodAllowedInImage); m_d->animationPlayer = new KisAnimationPlayer(this); connect(m_d->view->canvasController()->proxyObject, SIGNAL(moveDocumentOffset(QPoint)), SLOT(documentOffsetMoved(QPoint))); connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); /** * We switch the shape manager every time vector layer or * shape selection is activated. Flake does not expect this * and connects all the signals of the global shape manager * to the clients in the constructor. To workaround this we * forward the signals of local shape managers stored in the * vector layers to the signals of global shape manager. So the * sequence of signal deliveries is the following: * * shapeLayer.m_d.canvas.m_shapeManager.selection() -> * shapeLayer -> * shapeController -> * globalShapeManager.selection() */ KisShapeController *kritaShapeController = static_cast(shapeController()->documentBase()); connect(kritaShapeController, SIGNAL(selectionChanged()), this, SLOT(slotSelectionChanged())); connect(kritaShapeController, SIGNAL(selectionContentChanged()), selectedShapesProxy(), SIGNAL(selectionContentChanged())); connect(kritaShapeController, SIGNAL(currentLayerChanged(const KoShapeLayer*)), selectedShapesProxy(), SIGNAL(currentLayerChanged(const KoShapeLayer*))); connect(&m_d->canvasUpdateCompressor, SIGNAL(timeout()), SLOT(slotDoCanvasUpdate())); connect(this, SIGNAL(sigCanvasCacheUpdated()), &m_d->frameRenderStartCompressor, SLOT(start())); connect(&m_d->frameRenderStartCompressor, SIGNAL(timeout()), SLOT(updateCanvasProjection())); connect(this, SIGNAL(sigContinueResizeImage(qint32,qint32)), SLOT(finishResizingImage(qint32,qint32))); connect(&m_d->regionOfInterestUpdateCompressor, SIGNAL(timeout()), SLOT(slotUpdateRegionOfInterest())); connect(m_d->view->document(), SIGNAL(sigReferenceImagesChanged()), this, SLOT(slotReferenceImagesChanged())); initializeFpsDecoration(); } void KisCanvas2::initializeFpsDecoration() { KisConfig cfg(true); const bool shouldShowDebugOverlay = (canvasIsOpenGL() && cfg.enableOpenGLFramerateLogging()) || cfg.enableBrushSpeedLogging(); if (shouldShowDebugOverlay && !decoration(KisFpsDecoration::idTag)) { addDecoration(new KisFpsDecoration(imageView())); if (cfg.enableBrushSpeedLogging()) { connect(KisStrokeSpeedMonitor::instance(), SIGNAL(sigStatsUpdated()), this, SLOT(updateCanvas())); } } else if (!shouldShowDebugOverlay && decoration(KisFpsDecoration::idTag)) { m_d->canvasWidget->removeDecoration(KisFpsDecoration::idTag); disconnect(KisStrokeSpeedMonitor::instance(), SIGNAL(sigStatsUpdated()), this, SLOT(updateCanvas())); } } KisCanvas2::~KisCanvas2() { if (m_d->animationPlayer->isPlaying()) { m_d->animationPlayer->forcedStopOnExit(); } delete m_d; } void KisCanvas2::setCanvasWidget(KisAbstractCanvasWidget *widget) { if (m_d->popupPalette) { m_d->popupPalette->setParent(widget->widget()); } if (m_d->canvasWidget != 0) { widget->setDecorations(m_d->canvasWidget->decorations()); // Redundant check for the constructor case, see below if(viewManager() != 0) viewManager()->inputManager()->removeTrackedCanvas(this); } m_d->canvasWidget = widget; // Either tmp was null or we are being called by KisCanvas2 constructor that is called by KisView // constructor, so the view manager still doesn't exists. if(m_d->canvasWidget != 0 && viewManager() != 0) viewManager()->inputManager()->addTrackedCanvas(this); if (!m_d->canvasWidget->decoration(INFINITY_DECORATION_ID)) { KisInfinityManager *manager = new KisInfinityManager(m_d->view, this); manager->setVisible(true); m_d->canvasWidget->addDecoration(manager); } widget->widget()->setAutoFillBackground(false); widget->widget()->setAttribute(Qt::WA_OpaquePaintEvent); widget->widget()->setMouseTracking(true); widget->widget()->setAcceptDrops(true); KoCanvasControllerWidget *controller = dynamic_cast(canvasController()); if (controller && controller->canvas() == this) { controller->changeCanvasWidget(widget->widget()); } } bool KisCanvas2::canvasIsOpenGL() const { return m_d->currentCanvasIsOpenGL; } KisOpenGL::FilterMode KisCanvas2::openGLFilterMode() const { return KisOpenGL::FilterMode(m_d->openGLFilterMode); } void KisCanvas2::gridSize(QPointF *offset, QSizeF *spacing) const { QTransform transform = coordinatesConverter()->imageToDocumentTransform(); const QPoint intSpacing = m_d->view->document()->gridConfig().spacing(); const QPoint intOffset = m_d->view->document()->gridConfig().offset(); QPointF size = transform.map(QPointF(intSpacing)); spacing->rwidth() = size.x(); spacing->rheight() = size.y(); *offset = transform.map(QPointF(intOffset)); } bool KisCanvas2::snapToGrid() const { return m_d->view->document()->gridConfig().snapToGrid(); } qreal KisCanvas2::rotationAngle() const { return m_d->coordinatesConverter->rotationAngle(); } bool KisCanvas2::xAxisMirrored() const { return m_d->coordinatesConverter->xAxisMirrored(); } bool KisCanvas2::yAxisMirrored() const { return m_d->coordinatesConverter->yAxisMirrored(); } void KisCanvas2::channelSelectionChanged() { KisImageSP image = this->image(); m_d->channelFlags = image->rootLayer()->channelFlags(); m_d->view->viewManager()->blockUntilOperationsFinishedForced(image); image->barrierLock(); m_d->canvasWidget->channelSelectionChanged(m_d->channelFlags); startUpdateInPatches(image->bounds()); image->unlock(); } void KisCanvas2::addCommand(KUndo2Command *command) { // This method exists to support flake-related operations m_d->view->document()->addCommand(command); } void KisCanvas2::KisCanvas2Private::setActiveShapeManager(KoShapeManager *shapeManager) { if (shapeManager != currentlyActiveShapeManager) { currentlyActiveShapeManager = shapeManager; selectedShapesProxy.setShapeManager(shapeManager); } } KoShapeManager* KisCanvas2::shapeManager() const { KoShapeManager *localShapeManager = this->localShapeManager(); // sanity check for consistency of the local shape manager KIS_SAFE_ASSERT_RECOVER (localShapeManager == m_d->currentlyActiveShapeManager) { localShapeManager = globalShapeManager(); } return localShapeManager ? localShapeManager : globalShapeManager(); } KoSelectedShapesProxy* KisCanvas2::selectedShapesProxy() const { return &m_d->selectedShapesProxy; } KoShapeManager* KisCanvas2::globalShapeManager() const { return &m_d->shapeManager; } KoShapeManager *KisCanvas2::localShapeManager() const { KisNodeSP node = m_d->view->currentNode(); KoShapeManager *localShapeManager = fetchShapeManagerFromNode(node); if (localShapeManager != m_d->currentlyActiveShapeManager) { m_d->setActiveShapeManager(localShapeManager); } return localShapeManager; } void KisCanvas2::updateInputMethodInfo() { // TODO call (the protected) QWidget::updateMicroFocus() on the proper canvas widget... } const KisCoordinatesConverter* KisCanvas2::coordinatesConverter() const { return m_d->coordinatesConverter; } KoViewConverter* KisCanvas2::viewConverter() const { return m_d->coordinatesConverter; } KisInputManager* KisCanvas2::globalInputManager() const { return m_d->view->globalInputManager(); } KisInputActionGroupsMask KisCanvas2::inputActionGroupsMask() const { return m_d->inputActionGroupsMask; } void KisCanvas2::setInputActionGroupsMask(KisInputActionGroupsMask mask) { m_d->inputActionGroupsMask = mask; } QWidget* KisCanvas2::canvasWidget() { return m_d->canvasWidget->widget(); } const QWidget* KisCanvas2::canvasWidget() const { return m_d->canvasWidget->widget(); } KoUnit KisCanvas2::unit() const { KoUnit unit(KoUnit::Pixel); KisImageWSP image = m_d->view->image(); if (image) { if (!qFuzzyCompare(image->xRes(), image->yRes())) { warnKrita << "WARNING: resolution of the image is anisotropic" << ppVar(image->xRes()) << ppVar(image->yRes()); } const qreal resolution = image->xRes(); unit.setFactor(resolution); } return unit; } KoToolProxy * KisCanvas2::toolProxy() const { return &m_d->toolProxy; } void KisCanvas2::createQPainterCanvas() { m_d->currentCanvasIsOpenGL = false; KisQPainterCanvas * canvasWidget = new KisQPainterCanvas(this, m_d->coordinatesConverter, m_d->view); m_d->prescaledProjection = new KisPrescaledProjection(); m_d->prescaledProjection->setCoordinatesConverter(m_d->coordinatesConverter); m_d->prescaledProjection->setMonitorProfile(m_d->displayColorConverter.monitorProfile(), m_d->displayColorConverter.renderingIntent(), m_d->displayColorConverter.conversionFlags()); m_d->prescaledProjection->setDisplayFilter(m_d->displayColorConverter.displayFilter()); canvasWidget->setPrescaledProjection(m_d->prescaledProjection); setCanvasWidget(canvasWidget); } void KisCanvas2::createOpenGLCanvas() { KisConfig cfg(true); m_d->openGLFilterMode = cfg.openGLFilteringMode(); m_d->currentCanvasIsOpenGL = true; KisOpenGLCanvas2 *canvasWidget = new KisOpenGLCanvas2(this, m_d->coordinatesConverter, 0, m_d->view->image(), &m_d->displayColorConverter); m_d->frameCache = KisAnimationFrameCache::getFrameCache(canvasWidget->openGLImageTextures()); setCanvasWidget(canvasWidget); } void KisCanvas2::createCanvas(bool useOpenGL) { // deinitialize previous canvas structures m_d->prescaledProjection = 0; m_d->frameCache = 0; KisConfig cfg(true); QDesktopWidget dw; const KoColorProfile *profile = cfg.displayProfile(dw.screenNumber(imageView())); m_d->displayColorConverter.notifyOpenGLCanvasIsActive(useOpenGL && KisOpenGL::hasOpenGL()); m_d->displayColorConverter.setMonitorProfile(profile); if (useOpenGL && !KisOpenGL::hasOpenGL()) { warnKrita << "Tried to create OpenGL widget when system doesn't have OpenGL\n"; useOpenGL = false; } m_d->displayColorConverter.notifyOpenGLCanvasIsActive(useOpenGL); if (useOpenGL) { createOpenGLCanvas(); if (cfg.canvasState() == "OPENGL_FAILED") { // Creating the opengl canvas failed, fall back warnKrita << "OpenGL Canvas initialization returned OPENGL_FAILED. Falling back to QPainter."; m_d->displayColorConverter.notifyOpenGLCanvasIsActive(false); createQPainterCanvas(); } } else { createQPainterCanvas(); } if (m_d->popupPalette) { m_d->popupPalette->setParent(m_d->canvasWidget->widget()); } } void KisCanvas2::initializeImage() { KisImageSP image = m_d->view->image(); m_d->displayColorConverter.setImageColorSpace(image->colorSpace()); m_d->coordinatesConverter->setImage(image); m_d->toolProxy.initializeImage(image); connect(image, SIGNAL(sigImageUpdated(QRect)), SLOT(startUpdateCanvasProjection(QRect)), Qt::DirectConnection); connect(image->signalRouter(), SIGNAL(sigNotifyBatchUpdateStarted()), SLOT(slotBeginUpdatesBatch()), Qt::DirectConnection); connect(image->signalRouter(), SIGNAL(sigNotifyBatchUpdateEnded()), SLOT(slotEndUpdatesBatch()), Qt::DirectConnection); connect(image->signalRouter(), SIGNAL(sigRequestLodPlanesSyncBlocked(bool)), SLOT(slotSetLodUpdatesBlocked(bool)), Qt::DirectConnection); connect(image, SIGNAL(sigProofingConfigChanged()), SLOT(slotChangeProofingConfig())); connect(image, SIGNAL(sigSizeChanged(QPointF,QPointF)), SLOT(startResizingImage()), Qt::DirectConnection); connect(image->undoAdapter(), SIGNAL(selectionChanged()), SLOT(slotTrySwitchShapeManager())); connect(image, SIGNAL(sigColorSpaceChanged(const KoColorSpace*)), SLOT(slotImageColorSpaceChanged())); connect(image, SIGNAL(sigProfileChanged(const KoColorProfile*)), SLOT(slotImageColorSpaceChanged())); connectCurrentCanvas(); } void KisCanvas2::connectCurrentCanvas() { KisImageWSP image = m_d->view->image(); if (!m_d->currentCanvasIsOpenGL) { Q_ASSERT(m_d->prescaledProjection); m_d->prescaledProjection->setImage(image); } startResizingImage(); setLodAllowedInCanvas(m_d->lodAllowedInImage); emit sigCanvasEngineChanged(); } void KisCanvas2::resetCanvas(bool useOpenGL) { // we cannot reset the canvas before it's created, but this method might be called, // for instance when setting the monitor profile. if (!m_d->canvasWidget) { return; } KisConfig cfg(true); bool needReset = (m_d->currentCanvasIsOpenGL != useOpenGL) || (m_d->currentCanvasIsOpenGL && m_d->openGLFilterMode != cfg.openGLFilteringMode()); if (needReset) { createCanvas(useOpenGL); connectCurrentCanvas(); notifyZoomChanged(); } updateCanvasWidgetImpl(); } void KisCanvas2::startUpdateInPatches(const QRect &imageRect) { /** - * We don't do patched loading for openGL canvas, becasue it loads - * the tiles, which are bascially "patches". Therefore, big chunks + * We don't do patched loading for openGL canvas, because it loads + * the tiles, which are basically "patches". Therefore, big chunks * of memory are never allocated. */ if (m_d->currentCanvasIsOpenGL) { startUpdateCanvasProjection(imageRect); } else { KisImageConfig imageConfig(true); int patchWidth = imageConfig.updatePatchWidth(); int patchHeight = imageConfig.updatePatchHeight(); for (int y = 0; y < imageRect.height(); y += patchHeight) { for (int x = 0; x < imageRect.width(); x += patchWidth) { QRect patchRect(x, y, patchWidth, patchHeight); startUpdateCanvasProjection(patchRect); } } } } void KisCanvas2::setDisplayFilter(QSharedPointer displayFilter) { m_d->displayColorConverter.setDisplayFilter(displayFilter); KisImageSP image = this->image(); m_d->view->viewManager()->blockUntilOperationsFinishedForced(image); image->barrierLock(); m_d->canvasWidget->setDisplayFilter(displayFilter); image->unlock(); } QSharedPointer KisCanvas2::displayFilter() const { return m_d->displayColorConverter.displayFilter(); } void KisCanvas2::slotImageColorSpaceChanged() { KisImageSP image = this->image(); m_d->view->viewManager()->blockUntilOperationsFinishedForced(image); m_d->displayColorConverter.setImageColorSpace(image->colorSpace()); image->barrierLock(); m_d->canvasWidget->notifyImageColorSpaceChanged(image->colorSpace()); image->unlock(); } KisDisplayColorConverter* KisCanvas2::displayColorConverter() const { return &m_d->displayColorConverter; } KisExposureGammaCorrectionInterface* KisCanvas2::exposureGammaCorrectionInterface() const { QSharedPointer displayFilter = m_d->displayColorConverter.displayFilter(); return displayFilter ? displayFilter->correctionInterface() : KisDumbExposureGammaCorrectionInterface::instance(); } void KisCanvas2::setProofingOptions(bool softProof, bool gamutCheck) { m_d->proofingConfig = this->image()->proofingConfiguration(); if (!m_d->proofingConfig) { KisImageConfig cfg(false); m_d->proofingConfig = cfg.defaultProofingconfiguration(); } KoColorConversionTransformation::ConversionFlags conversionFlags = m_d->proofingConfig->conversionFlags; if (this->image()->colorSpace()->colorDepthId().id().contains("U")) { conversionFlags.setFlag(KoColorConversionTransformation::SoftProofing, softProof); if (softProof) { conversionFlags.setFlag(KoColorConversionTransformation::GamutCheck, gamutCheck); } } m_d->proofingConfig->conversionFlags = conversionFlags; m_d->proofingConfigUpdated = true; startUpdateInPatches(this->image()->bounds()); } void KisCanvas2::slotSoftProofing(bool softProofing) { m_d->softProofing = softProofing; setProofingOptions(m_d->softProofing, m_d->gamutCheck); } void KisCanvas2::slotGamutCheck(bool gamutCheck) { m_d->gamutCheck = gamutCheck; setProofingOptions(m_d->softProofing, m_d->gamutCheck); } void KisCanvas2::slotChangeProofingConfig() { setProofingOptions(m_d->softProofing, m_d->gamutCheck); } void KisCanvas2::setProofingConfigUpdated(bool updated) { m_d->proofingConfigUpdated = updated; } bool KisCanvas2::proofingConfigUpdated() { return m_d->proofingConfigUpdated; } KisProofingConfigurationSP KisCanvas2::proofingConfiguration() const { if (!m_d->proofingConfig) { m_d->proofingConfig = this->image()->proofingConfiguration(); if (!m_d->proofingConfig) { m_d->proofingConfig = KisImageConfig(true).defaultProofingconfiguration(); } } return m_d->proofingConfig; } void KisCanvas2::startResizingImage() { KisImageWSP image = this->image(); qint32 w = image->width(); qint32 h = image->height(); emit sigContinueResizeImage(w, h); QRect imageBounds(0, 0, w, h); startUpdateInPatches(imageBounds); } void KisCanvas2::finishResizingImage(qint32 w, qint32 h) { m_d->canvasWidget->finishResizingImage(w, h); } void KisCanvas2::startUpdateCanvasProjection(const QRect & rc) { KisUpdateInfoSP info = m_d->canvasWidget->startUpdateCanvasProjection(rc, m_d->channelFlags); if (m_d->projectionUpdatesCompressor.putUpdateInfo(info)) { emit sigCanvasCacheUpdated(); } } void KisCanvas2::updateCanvasProjection() { auto tryIssueCanvasUpdates = [this](const QRect &vRect) { if (!m_d->isBatchUpdateActive) { // TODO: Implement info->dirtyViewportRect() for KisOpenGLCanvas2 to avoid updating whole canvas if (m_d->currentCanvasIsOpenGL) { m_d->savedUpdateRect = QRect(); // we already had a compression in frameRenderStartCompressor, so force the update directly slotDoCanvasUpdate(); } else if (/* !m_d->currentCanvasIsOpenGL && */ !vRect.isEmpty()) { m_d->savedUpdateRect = m_d->coordinatesConverter->viewportToWidget(vRect).toAlignedRect(); // we already had a compression in frameRenderStartCompressor, so force the update directly slotDoCanvasUpdate(); } } }; auto uploadData = [this, tryIssueCanvasUpdates](const QVector &infoObjects) { QVector viewportRects = m_d->canvasWidget->updateCanvasProjection(infoObjects); const QRect vRect = std::accumulate(viewportRects.constBegin(), viewportRects.constEnd(), QRect(), std::bit_or()); tryIssueCanvasUpdates(vRect); }; bool shouldExplicitlyIssueUpdates = false; QVector infoObjects; KisUpdateInfoList originalInfoObjects; m_d->projectionUpdatesCompressor.takeUpdateInfo(originalInfoObjects); for (auto it = originalInfoObjects.constBegin(); it != originalInfoObjects.constEnd(); ++it) { KisUpdateInfoSP info = *it; const KisMarkerUpdateInfo *batchInfo = dynamic_cast(info.data()); if (batchInfo) { if (!infoObjects.isEmpty()) { uploadData(infoObjects); infoObjects.clear(); } if (batchInfo->type() == KisMarkerUpdateInfo::StartBatch) { m_d->isBatchUpdateActive++; } else if (batchInfo->type() == KisMarkerUpdateInfo::EndBatch) { m_d->isBatchUpdateActive--; KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->isBatchUpdateActive >= 0); if (m_d->isBatchUpdateActive == 0) { shouldExplicitlyIssueUpdates = true; } } else if (batchInfo->type() == KisMarkerUpdateInfo::BlockLodUpdates) { m_d->canvasWidget->setLodResetInProgress(true); } else if (batchInfo->type() == KisMarkerUpdateInfo::UnblockLodUpdates) { m_d->canvasWidget->setLodResetInProgress(false); shouldExplicitlyIssueUpdates = true; } } else { infoObjects << info; } } if (!infoObjects.isEmpty()) { uploadData(infoObjects); } else if (shouldExplicitlyIssueUpdates) { tryIssueCanvasUpdates(m_d->coordinatesConverter->imageRectInImagePixels()); } } void KisCanvas2::slotBeginUpdatesBatch() { KisUpdateInfoSP info = new KisMarkerUpdateInfo(KisMarkerUpdateInfo::StartBatch, m_d->coordinatesConverter->imageRectInImagePixels()); m_d->projectionUpdatesCompressor.putUpdateInfo(info); emit sigCanvasCacheUpdated(); } void KisCanvas2::slotEndUpdatesBatch() { KisUpdateInfoSP info = new KisMarkerUpdateInfo(KisMarkerUpdateInfo::EndBatch, m_d->coordinatesConverter->imageRectInImagePixels()); m_d->projectionUpdatesCompressor.putUpdateInfo(info); emit sigCanvasCacheUpdated(); } void KisCanvas2::slotSetLodUpdatesBlocked(bool value) { KisUpdateInfoSP info = new KisMarkerUpdateInfo(value ? KisMarkerUpdateInfo::BlockLodUpdates : KisMarkerUpdateInfo::UnblockLodUpdates, m_d->coordinatesConverter->imageRectInImagePixels()); m_d->projectionUpdatesCompressor.putUpdateInfo(info); emit sigCanvasCacheUpdated(); } void KisCanvas2::slotDoCanvasUpdate() { /** * WARNING: in isBusy() we access openGL functions without making the painting * context current. We hope that currently active context will be Qt's one, * which is shared with our own. */ if (m_d->canvasWidget->isBusy()) { // just restarting the timer updateCanvasWidgetImpl(m_d->savedUpdateRect); return; } if (m_d->savedUpdateRect.isEmpty()) { m_d->canvasWidget->widget()->update(); emit updateCanvasRequested(m_d->canvasWidget->widget()->rect()); } else { emit updateCanvasRequested(m_d->savedUpdateRect); m_d->canvasWidget->widget()->update(m_d->savedUpdateRect); } m_d->savedUpdateRect = QRect(); } void KisCanvas2::updateCanvasWidgetImpl(const QRect &rc) { if (!m_d->canvasUpdateCompressor.isActive() || !m_d->savedUpdateRect.isEmpty()) { m_d->savedUpdateRect |= rc; } m_d->canvasUpdateCompressor.start(); } void KisCanvas2::updateCanvas() { updateCanvasWidgetImpl(); } void KisCanvas2::updateCanvas(const QRectF& documentRect) { if (m_d->currentCanvasIsOpenGL && m_d->canvasWidget->decorations().size() > 0) { updateCanvasWidgetImpl(); } else { // updateCanvas is called from tools, never from the projection // updates, so no need to prescale! QRect widgetRect = m_d->coordinatesConverter->documentToWidget(documentRect).toAlignedRect(); widgetRect.adjust(-2, -2, 2, 2); if (!widgetRect.isEmpty()) { updateCanvasWidgetImpl(widgetRect); } } } void KisCanvas2::disconnectCanvasObserver(QObject *object) { KoCanvasBase::disconnectCanvasObserver(object); m_d->view->disconnect(object); } void KisCanvas2::notifyZoomChanged() { if (!m_d->currentCanvasIsOpenGL) { Q_ASSERT(m_d->prescaledProjection); m_d->prescaledProjection->notifyZoomChanged(); } notifyLevelOfDetailChange(); updateCanvas(); // update the canvas, because that isn't done when zooming using KoZoomAction m_d->regionOfInterestUpdateCompressor.start(); } QRect KisCanvas2::regionOfInterest() const { return m_d->regionOfInterest; } void KisCanvas2::slotUpdateRegionOfInterest() { const QRect oldRegionOfInterest = m_d->regionOfInterest; const qreal ratio = m_d->regionOfInterestMargin; const QRect proposedRoi = KisAlgebra2D::blowRect(m_d->coordinatesConverter->widgetRectInImagePixels(), ratio).toAlignedRect(); const QRect imageRect = m_d->coordinatesConverter->imageRectInImagePixels(); m_d->regionOfInterest = proposedRoi & imageRect; if (m_d->regionOfInterest != oldRegionOfInterest) { emit sigRegionOfInterestChanged(m_d->regionOfInterest); } } void KisCanvas2::slotReferenceImagesChanged() { canvasController()->resetScrollBars(); } void KisCanvas2::setRenderingLimit(const QRect &rc) { m_d->renderingLimit = rc; } QRect KisCanvas2::renderingLimit() const { return m_d->renderingLimit; } void KisCanvas2::slotTrySwitchShapeManager() { KisNodeSP node = m_d->view->currentNode(); QPointer newManager; newManager = fetchShapeManagerFromNode(node); m_d->setActiveShapeManager(newManager); } void KisCanvas2::notifyLevelOfDetailChange() { if (!m_d->effectiveLodAllowedInImage()) return; const qreal effectiveZoom = m_d->coordinatesConverter->effectiveZoom(); KisConfig cfg(true); const int maxLod = cfg.numMipmapLevels(); const int lod = KisLodTransform::scaleToLod(effectiveZoom, maxLod); if (m_d->effectiveLodAllowedInImage()) { KisImageSP image = this->image(); image->setDesiredLevelOfDetail(lod); } } const KoColorProfile * KisCanvas2::monitorProfile() { return m_d->displayColorConverter.monitorProfile(); } KisViewManager* KisCanvas2::viewManager() const { if (m_d->view) { return m_d->view->viewManager(); } return 0; } QPointerKisCanvas2::imageView() const { return m_d->view; } KisImageWSP KisCanvas2::image() const { return m_d->view->image(); } KisImageWSP KisCanvas2::currentImage() const { return m_d->view->image(); } void KisCanvas2::documentOffsetMoved(const QPoint &documentOffset) { QPointF offsetBefore = m_d->coordinatesConverter->imageRectInViewportPixels().topLeft(); // The given offset is in widget logical pixels. In order to prevent fuzzy // canvas rendering at 100% pixel-perfect zoom level when devicePixelRatio // is not integral, we adjusts the offset to map to whole device pixels. // // FIXME: This is a temporary hack for fixing the canvas under fractional // DPI scaling before a new coordinate system is introduced. QPointF offsetAdjusted = m_d->coordinatesConverter->snapToDevicePixel(documentOffset); m_d->coordinatesConverter->setDocumentOffset(offsetAdjusted); QPointF offsetAfter = m_d->coordinatesConverter->imageRectInViewportPixels().topLeft(); QPointF moveOffset = offsetAfter - offsetBefore; if (!m_d->currentCanvasIsOpenGL) m_d->prescaledProjection->viewportMoved(moveOffset); emit documentOffsetUpdateFinished(); updateCanvas(); m_d->regionOfInterestUpdateCompressor.start(); } void KisCanvas2::slotConfigChanged() { KisConfig cfg(true); m_d->vastScrolling = cfg.vastScrolling(); m_d->regionOfInterestMargin = KisImageConfig(true).animationCacheRegionOfInterestMargin(); resetCanvas(cfg.useOpenGL()); // HACK: Sometimes screenNumber(this->canvasWidget()) is not able to get the // proper screenNumber when moving the window across screens. Using // the coordinates should be able to work around this. // FIXME: We should change to associate the display profiles with the screen // model and serial number instead. See https://bugs.kde.org/show_bug.cgi?id=407498 int canvasScreenNumber = QApplication::desktop()->screenNumber(this->canvasWidget()); if (canvasScreenNumber != -1) { setDisplayProfile(cfg.displayProfile(canvasScreenNumber)); } else { warnUI << "Failed to get screenNumber for updating display profile."; } initializeFpsDecoration(); } void KisCanvas2::refetchDataFromImage() { KisImageSP image = this->image(); KisImageBarrierLocker l(image); startUpdateInPatches(image->bounds()); } void KisCanvas2::setDisplayProfile(const KoColorProfile *monitorProfile) { if (m_d->displayColorConverter.monitorProfile() == monitorProfile) return; m_d->displayColorConverter.setMonitorProfile(monitorProfile); { KisImageSP image = this->image(); KisImageBarrierLocker l(image); m_d->canvasWidget->setDisplayColorConverter(&m_d->displayColorConverter); } refetchDataFromImage(); } void KisCanvas2::addDecoration(KisCanvasDecorationSP deco) { m_d->canvasWidget->addDecoration(deco); } KisCanvasDecorationSP KisCanvas2::decoration(const QString& id) const { return m_d->canvasWidget->decoration(id); } QPoint KisCanvas2::documentOrigin() const { /** * In Krita we don't use document origin anymore. * All the centering when needed (vastScrolling < 0.5) is done * automatically by the KisCoordinatesConverter. */ return QPoint(); } QPoint KisCanvas2::documentOffset() const { return m_d->coordinatesConverter->documentOffset(); } void KisCanvas2::setFavoriteResourceManager(KisFavoriteResourceManager* favoriteResourceManager) { m_d->popupPalette = new KisPopupPalette(viewManager(), m_d->coordinatesConverter, favoriteResourceManager, displayColorConverter()->displayRendererInterface(), m_d->view->resourceProvider(), m_d->canvasWidget->widget()); connect(m_d->popupPalette, SIGNAL(zoomLevelChanged(int)), this, SLOT(slotPopupPaletteRequestedZoomChange(int))); connect(m_d->popupPalette, SIGNAL(sigUpdateCanvas()), this, SLOT(updateCanvas())); connect(m_d->view->mainWindow(), SIGNAL(themeChanged()), m_d->popupPalette, SLOT(slotUpdateIcons())); m_d->popupPalette->showPopupPalette(false); } void KisCanvas2::slotPopupPaletteRequestedZoomChange(int zoom ) { m_d->view->viewManager()->zoomController()->setZoom(KoZoomMode::ZOOM_CONSTANT, (qreal)(zoom/100.0)); // 1.0 is 100% zoom notifyZoomChanged(); } void KisCanvas2::setCursor(const QCursor &cursor) { canvasWidget()->setCursor(cursor); } KisAnimationFrameCacheSP KisCanvas2::frameCache() const { return m_d->frameCache; } KisAnimationPlayer *KisCanvas2::animationPlayer() const { return m_d->animationPlayer; } void KisCanvas2::slotSelectionChanged() { KisShapeLayer* shapeLayer = dynamic_cast(viewManager()->activeLayer().data()); if (!shapeLayer) { return; } m_d->shapeManager.selection()->deselectAll(); Q_FOREACH (KoShape* shape, shapeLayer->shapeManager()->selection()->selectedShapes()) { m_d->shapeManager.selection()->select(shape); } } bool KisCanvas2::isPopupPaletteVisible() const { if (!m_d->popupPalette) { return false; } return m_d->popupPalette->isVisible(); } void KisCanvas2::setWrapAroundViewingMode(bool value) { KisCanvasDecorationSP infinityDecoration = m_d->canvasWidget->decoration(INFINITY_DECORATION_ID); if (infinityDecoration) { infinityDecoration->setVisible(!value); } m_d->canvasWidget->setWrapAroundViewingMode(value); } bool KisCanvas2::wrapAroundViewingMode() const { KisCanvasDecorationSP infinityDecoration = m_d->canvasWidget->decoration(INFINITY_DECORATION_ID); if (infinityDecoration) { return !(infinityDecoration->visible()); } return false; } void KisCanvas2::bootstrapFinished() { if (!m_d->bootstrapLodBlocked) return; m_d->bootstrapLodBlocked = false; setLodAllowedInCanvas(m_d->lodAllowedInImage); } void KisCanvas2::setLodAllowedInCanvas(bool value) { if (!KisOpenGL::supportsLoD()) { qWarning() << "WARNING: Level of Detail functionality is available only with openGL + GLSL 1.3 support"; } m_d->lodAllowedInImage = value && m_d->currentCanvasIsOpenGL && KisOpenGL::supportsLoD() && (m_d->openGLFilterMode == KisOpenGL::TrilinearFilterMode || m_d->openGLFilterMode == KisOpenGL::HighQualityFiltering); KisImageSP image = this->image(); if (m_d->effectiveLodAllowedInImage() != !image->levelOfDetailBlocked()) { image->setLevelOfDetailBlocked(!m_d->effectiveLodAllowedInImage()); } notifyLevelOfDetailChange(); KisConfig cfg(false); cfg.setLevelOfDetailEnabled(m_d->lodAllowedInImage); KisUsageLogger::log(QString("Instant Preview Setting: %1").arg(m_d->lodAllowedInImage)); } bool KisCanvas2::lodAllowedInCanvas() const { return m_d->lodAllowedInImage; } void KisCanvas2::slotShowPopupPalette(const QPoint &p) { if (!m_d->popupPalette) { return; } m_d->popupPalette->showPopupPalette(p); } KisPaintingAssistantsDecorationSP KisCanvas2::paintingAssistantsDecoration() const { KisCanvasDecorationSP deco = decoration("paintingAssistantsDecoration"); return qobject_cast(deco.data()); } KisReferenceImagesDecorationSP KisCanvas2::referenceImagesDecoration() const { KisCanvasDecorationSP deco = decoration("referenceImagesDecoration"); return qobject_cast(deco.data()); } diff --git a/libs/ui/canvas/kis_grid_config.h b/libs/ui/canvas/kis_grid_config.h index 9042fba49a..a052593774 100644 --- a/libs/ui/canvas/kis_grid_config.h +++ b/libs/ui/canvas/kis_grid_config.h @@ -1,257 +1,257 @@ /* * Copyright (c) 2015 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 __KIS_GRID_CONFIG_H #define __KIS_GRID_CONFIG_H #include #include #include #include #include "kritaui_export.h" class QDomElement; class QDomDocument; class KRITAUI_EXPORT KisGridConfig : boost::equality_comparable { public: enum LineTypeInternal { LINE_SOLID = 0, LINE_DASHED, LINE_DOTTED }; enum GridType { GRID_RECTANGULAR = 0, GRID_ISOMETRIC }; public: KisGridConfig() : m_showGrid(false), m_snapToGrid(false), m_spacing(20,20), m_offsetAspectLocked(true), m_spacingAspectLocked(true), m_angleLeft(45), m_angleRight(45), m_cellSpacing(30), m_gridType(GRID_RECTANGULAR), m_subdivision(2), m_lineTypeMain(LINE_SOLID), m_lineTypeSubdivision(LINE_DOTTED), m_colorMain(200, 200, 200, 200), m_colorSubdivision(200, 200, 200, 150) { loadStaticData(); } bool operator==(const KisGridConfig &rhs) const { return m_showGrid == rhs.m_showGrid && m_snapToGrid == rhs.m_snapToGrid && m_spacing == rhs.m_spacing && m_offset == rhs.m_offset && m_offsetAspectLocked == rhs.m_offsetAspectLocked && m_spacingAspectLocked == rhs.m_spacingAspectLocked && m_angleRight == rhs.m_angleRight && m_angleLeft == rhs.m_angleLeft && m_gridType == rhs.m_gridType && m_cellSpacing == rhs.m_cellSpacing && m_subdivision == rhs.m_subdivision && m_lineTypeMain == rhs.m_lineTypeMain && m_lineTypeSubdivision == rhs.m_lineTypeSubdivision && m_colorMain == rhs.m_colorMain && m_colorSubdivision == rhs.m_colorSubdivision; } bool showGrid() const { return m_showGrid; } void setShowGrid(bool value) { m_showGrid = value; } bool snapToGrid() const { return m_snapToGrid; } void setSnapToGrid(bool value) { m_snapToGrid = value; } QPoint offset() const { return m_offset; } void setOffset(const QPoint &value) { m_offset = value; } QPoint spacing() const { return m_spacing; } void setSpacing(const QPoint &value) { m_spacing = value; } int subdivision() const { return m_subdivision; } void setSubdivision(int value) { m_subdivision = value; } int angleLeft() const { return m_angleLeft; } void setAngleLeft(int angle) { m_angleLeft = angle; } int angleRight() const { return m_angleRight; } void setAngleRight(int angle) { m_angleRight = angle; } int cellSpacing() const { return m_cellSpacing; } void setCellSpacing(int spacing) { m_cellSpacing = spacing; } GridType gridType() const { return m_gridType; } void setGridType(GridType type) { m_gridType = type; } bool offsetAspectLocked() const { return m_offsetAspectLocked; } void setOffsetAspectLocked(bool value) { m_offsetAspectLocked = value; } bool spacingAspectLocked() const { return m_spacingAspectLocked; } void setSpacingAspectLocked(bool value) { m_spacingAspectLocked = value; } LineTypeInternal lineTypeMain() const { return m_lineTypeMain; } void setLineTypeMain(LineTypeInternal value) { m_lineTypeMain = value; } LineTypeInternal lineTypeSubdivision() const { return m_lineTypeSubdivision; } void setLineTypeSubdivision(LineTypeInternal value) { m_lineTypeSubdivision = value; } QColor colorMain() const { return m_colorMain; } void setColorMain(const QColor &value) { m_colorMain = value; } QColor colorSubdivision() const { return m_colorSubdivision; } void setColorSubdivision(const QColor &value) { m_colorSubdivision = value; } QPen penMain() const { return QPen(m_colorMain, 0, toPenStyle(m_lineTypeMain)); } QPen penSubdivision() const { return QPen(m_colorSubdivision, 0, toPenStyle(m_lineTypeSubdivision)); } void loadStaticData(); void saveStaticData() const; QDomElement saveDynamicDataToXml(QDomDocument& doc, const QString &tag) const; bool loadDynamicDataFromXml(const QDomElement &parent); static const KisGridConfig& defaultGrid(); bool isDefault() const { return *this == defaultGrid(); } - /// Transform the grids using the given \p tranform. Please note that \p transform + /// Transform the grids using the given \p transform. Please note that \p transform /// should be in 'image' coordinate system. /// Used with image-wide transformations. void transform(const QTransform &transform); private: static Qt::PenStyle toPenStyle(LineTypeInternal type) { return type == LINE_SOLID ? Qt::SolidLine : type == LINE_DASHED ? Qt::DashLine : type == LINE_DOTTED ? Qt::DotLine : Qt::DashDotDotLine; } private: // Dynamic data. Stored in KisDocument. bool m_showGrid; bool m_snapToGrid; QPoint m_spacing; bool m_offsetAspectLocked; bool m_spacingAspectLocked; int m_angleLeft; int m_angleRight; int m_cellSpacing; GridType m_gridType; int m_subdivision; QPoint m_offset; // Static data. Stored in the Krita config. LineTypeInternal m_lineTypeMain; LineTypeInternal m_lineTypeSubdivision; QColor m_colorMain; QColor m_colorSubdivision; }; Q_DECLARE_METATYPE(KisGridConfig) #endif /* __KIS_GRID_CONFIG_H */ diff --git a/libs/ui/canvas/kis_guides_config.h b/libs/ui/canvas/kis_guides_config.h index 7aff6e7eeb..3bf5cbc36d 100644 --- a/libs/ui/canvas/kis_guides_config.h +++ b/libs/ui/canvas/kis_guides_config.h @@ -1,136 +1,136 @@ /* This file is part of the KDE project Copyright (C) 2006 Laurent Montel Copyright (C) 2008 Jan Hambrecht Copyright (c) 2015 Dmitry Kazakov 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 KOGUIDESDATA_H #define KOGUIDESDATA_H #include "kritaui_export.h" #include #include #include #include class QDomElement; class QDomDocument; class QColor; class QPen; class KRITAUI_EXPORT KisGuidesConfig : boost::equality_comparable { public: enum LineTypeInternal { LINE_SOLID = 0, LINE_DASHED, LINE_DOTTED }; public: KisGuidesConfig(); ~KisGuidesConfig(); KisGuidesConfig(const KisGuidesConfig &rhs); KisGuidesConfig& operator=(const KisGuidesConfig &rhs); bool operator==(const KisGuidesConfig &rhs) const; bool hasSamePositionAs(const KisGuidesConfig &rhs) const; /** * @brief Set the positions of the horizontal guide lines * * @param lines a list of positions of the horizontal guide lines */ void setHorizontalGuideLines(const QList &lines); /** * @brief Set the positions of the vertical guide lines * * @param lines a list of positions of the vertical guide lines */ void setVerticalGuideLines(const QList &lines); /** * @brief Add a guide line to the canvas. * * @param orientation the orientation of the guide line * @param position the position in document coordinates of the guide line */ void addGuideLine(Qt::Orientation orientation, qreal position); /** * @brief Display or not guide lines */ bool showGuideLines() const; /** * @param show display or not guide line */ void setShowGuideLines(bool show); bool showGuides() const; void setShowGuides(bool value); bool lockGuides() const; void setLockGuides(bool value); bool snapToGuides() const; void setSnapToGuides(bool value); bool rulersMultiple2() const; void setRulersMultiple2(bool value); KoUnit::Type unitType() const; void setUnitType(KoUnit::Type type); LineTypeInternal guidesLineType() const; void setGuidesLineType(LineTypeInternal value); QColor guidesColor() const; void setGuidesColor(const QColor &value); QPen guidesPen() const; /// Returns the list of horizontal guide lines. const QList& horizontalGuideLines() const; /// Returns the list of vertical guide lines. const QList& verticalGuideLines() const; bool hasGuides() const; void loadStaticData(); void saveStaticData() const; QDomElement saveToXml(QDomDocument& doc, const QString &tag) const; bool loadFromXml(const QDomElement &parent); bool isDefault() const; - /// Transform the guides using the given \p tranform. Please note that \p transform + /// Transform the guides using the given \p transform. Please note that \p transform /// should be in 'document' coordinate system. /// Used with image-wide transformations. void transform(const QTransform &transform); private: class Private; const QScopedPointer d; }; #endif diff --git a/libs/ui/canvas/kis_image_patch.h b/libs/ui/canvas/kis_image_patch.h index 5ddcbc6f3a..76b671f257 100644 --- a/libs/ui/canvas/kis_image_patch.h +++ b/libs/ui/canvas/kis_image_patch.h @@ -1,111 +1,111 @@ /* * Copyright (c) 2010 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 KIS_IMAGE_PATCH_H_ #define KIS_IMAGE_PATCH_H_ #include #include #include #define BORDER_SIZE(scale) (ceil(0.5/scale)) class KisImagePatch { public: /** * A default constructor initializing invalid patch */ KisImagePatch(); /** * Initializes a new patch with given values. * Be careful, because the constructor does not fill * QImage of the patch, as the patch rect is not known yet * * \see setImage */ KisImagePatch(QRect imageRect, qint32 borderWidth, qreal scaleX, qreal scaleY); /** * Sets the image of the patch * Should be called right after the constructor * to finish initializing the object */ void setImage(QImage image); /** * prescale the patch image. Call after setImage(). * This ensures that we use the QImage smoothscale method, not the QPainter scaling, * which is far inferior. */ void preScale(const QRectF &dstRect); /** * Returns the rect of KisImage covered by the image * of the patch (in KisImage pixels) * * \see m_patchRect */ QRect patchRect(); /** * Draws an m_interestRect of the patch onto @p gc * By the way it fits this rect into @p dstRect * @p renderHints are directly transmitted to QPainter */ void drawMe(QPainter &gc, const QRectF &dstRect, QPainter::RenderHints renderHints); /** * Checks whether the patch can be used for drawing the image */ bool isValid(); private: /** * The scale of the image stored in the patch */ qreal m_scaleX; qreal m_scaleY; /** * The rect of KisImage covered by the image * of the patch (in KisImage pixels) */ QRect m_patchRect; /** * The rect that was requested during creation * of the patch. It equals to patchRect withount * borders - * These borders are introdused for more accurate + * These borders are introduced for more accurate * smooth scaling to reduce border effects * (IN m_image PIXELS, relative to m_image's topLeft); */ QRectF m_interestRect; QImage m_image; bool m_isScaled; }; #endif /* KIS_IMAGE_PATCH_H_ */ diff --git a/libs/ui/canvas/kis_prescaled_projection.cpp b/libs/ui/canvas/kis_prescaled_projection.cpp index a7212b7cf3..b276b4c474 100644 --- a/libs/ui/canvas/kis_prescaled_projection.cpp +++ b/libs/ui/canvas/kis_prescaled_projection.cpp @@ -1,400 +1,400 @@ /* * Copyright (c) 2007, Boudewijn Rempt * Copyright (c) 2008, Cyrille Berger * 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 "kis_prescaled_projection.h" #include #include #include #include #include #include #include #include #include #include "kis_config.h" #include "kis_image_config.h" #include "kis_config_notifier.h" #include "kis_image.h" #include "krita_utils.h" #include "kis_coordinates_converter.h" #include "kis_projection_backend.h" #include "kis_image_pyramid.h" #include "kis_display_filter.h" #define ceiledSize(sz) QSize(ceil((sz).width()), ceil((sz).height())) inline void copyQImageBuffer(uchar* dst, const uchar* src , qint32 deltaX, qint32 width) { if (deltaX >= 0) { memcpy(dst + 4 * deltaX, src, 4 *(width - deltaX) * sizeof(uchar)); } else { memcpy(dst, src - 4 * deltaX, 4 *(width + deltaX) * sizeof(uchar)); } } void copyQImage(qint32 deltaX, qint32 deltaY, QImage* dstImage, const QImage& srcImage) { qint32 height = dstImage->height(); qint32 width = dstImage->width(); Q_ASSERT(dstImage->width() == srcImage.width() && dstImage->height() == srcImage.height()); if (deltaY >= 0) { for (int y = 0; y < height - deltaY; y ++) { const uchar* src = srcImage.scanLine(y); uchar* dst = dstImage->scanLine(y + deltaY); copyQImageBuffer(dst, src, deltaX, width); } } else { for (int y = 0; y < height + deltaY; y ++) { const uchar* src = srcImage.scanLine(y - deltaY); uchar* dst = dstImage->scanLine(y); copyQImageBuffer(dst, src, deltaX, width); } } } struct KisPrescaledProjection::Private { Private() : viewportSize(0, 0) , projectionBackend(0) { } QImage prescaledQImage; QSize updatePatchSize; QSize canvasSize; QSize viewportSize; KisImageWSP image; KisCoordinatesConverter *coordinatesConverter; KisProjectionBackend* projectionBackend; }; KisPrescaledProjection::KisPrescaledProjection() : QObject(0) , m_d(new Private()) { updateSettings(); // we disable building the pyramid with setting its height to 1 // XXX: setting it higher than 1 is broken because it's not updated until you show/hide the layer m_d->projectionBackend = new KisImagePyramid(1); connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(updateSettings())); } KisPrescaledProjection::~KisPrescaledProjection() { delete m_d->projectionBackend; delete m_d; } void KisPrescaledProjection::setImage(KisImageWSP image) { Q_ASSERT(image); m_d->image = image; m_d->projectionBackend->setImage(image); } QImage KisPrescaledProjection::prescaledQImage() const { return m_d->prescaledQImage; } void KisPrescaledProjection::setCoordinatesConverter(KisCoordinatesConverter *coordinatesConverter) { m_d->coordinatesConverter = coordinatesConverter; } void KisPrescaledProjection::updateSettings() { KisImageConfig imageConfig(false); m_d->updatePatchSize.setWidth(imageConfig.updatePatchWidth()); m_d->updatePatchSize.setHeight(imageConfig.updatePatchHeight()); } void KisPrescaledProjection::viewportMoved(const QPointF &offset) { // FIXME: \|/ if (m_d->prescaledQImage.isNull()) return; if (offset.isNull()) return; QPoint alignedOffset = offset.toPoint(); if(offset != alignedOffset) { /** * We can't optimize anything when offset is float :( * Just prescale entire image. */ dbgRender << "prescaling the entire image because the offset is float"; preScale(); return; } QImage newImage = QImage(m_d->viewportSize, QImage::Format_ARGB32); newImage.fill(0); /** * TODO: viewport rects should be cropped by the borders of * the image, because it may be requested to read/write * outside QImage and copyQImage will not catch it */ QRect newViewportRect = QRect(QPoint(0,0), m_d->viewportSize); QRect oldViewportRect = newViewportRect.translated(alignedOffset); QRegion updateRegion = newViewportRect; QRect savedArea = newViewportRect & oldViewportRect; if(!savedArea.isEmpty()) { copyQImage(alignedOffset.x(), alignedOffset.y(), &newImage, m_d->prescaledQImage); updateRegion -= savedArea; } QPainter gc(&newImage); QVector rects = updateRegion.rects(); Q_FOREACH (const QRect &rect, rects) { QRect imageRect = m_d->coordinatesConverter->viewportToImage(rect).toAlignedRect(); QVector patches = KritaUtils::splitRectIntoPatches(imageRect, m_d->updatePatchSize); Q_FOREACH (const QRect& rc, patches) { QRect viewportPatch = m_d->coordinatesConverter->imageToViewport(rc).toAlignedRect(); KisPPUpdateInfoSP info = getInitialUpdateInformation(QRect()); fillInUpdateInformation(viewportPatch, info); drawUsingBackend(gc, info); } } m_d->prescaledQImage = newImage; } void KisPrescaledProjection::slotImageSizeChanged(qint32 w, qint32 h) { m_d->projectionBackend->setImageSize(w, h); // viewport size is cropped by the size of the image // so we need to update it as well updateViewportSize(); } KisUpdateInfoSP KisPrescaledProjection::updateCache(const QRect &dirtyImageRect) { if (!m_d->image) { dbgRender << "Calling updateCache without an image: " << kisBacktrace() << endl; // return invalid info return new KisPPUpdateInfo(); } /** - * We needn't this stuff ouside KisImage's area. We're not displaying + * We needn't this stuff outside KisImage's area. We're not displaying * anything painted outside the image anyway. */ QRect croppedImageRect = dirtyImageRect & m_d->image->bounds(); if (croppedImageRect.isEmpty()) return new KisPPUpdateInfo(); KisPPUpdateInfoSP info = getInitialUpdateInformation(croppedImageRect); m_d->projectionBackend->updateCache(croppedImageRect); return info; } void KisPrescaledProjection::recalculateCache(KisUpdateInfoSP info) { KisPPUpdateInfoSP ppInfo = dynamic_cast(info.data()); if(!ppInfo) return; QRect rawViewRect = m_d->coordinatesConverter-> imageToViewport(ppInfo->dirtyImageRectVar).toAlignedRect(); fillInUpdateInformation(rawViewRect, ppInfo); m_d->projectionBackend->recalculateCache(ppInfo); if(!info->dirtyViewportRect().isEmpty()) updateScaledImage(ppInfo); } void KisPrescaledProjection::preScale() { if (!m_d->image) return; m_d->prescaledQImage.fill(0); QRect viewportRect(QPoint(0, 0), m_d->viewportSize); QRect imageRect = m_d->coordinatesConverter->viewportToImage(viewportRect).toAlignedRect(); QVector patches = KritaUtils::splitRectIntoPatches(imageRect, m_d->updatePatchSize); Q_FOREACH (const QRect& rc, patches) { QRect viewportPatch = m_d->coordinatesConverter->imageToViewport(rc).toAlignedRect(); KisPPUpdateInfoSP info = getInitialUpdateInformation(QRect()); fillInUpdateInformation(viewportPatch, info); QPainter gc(&m_d->prescaledQImage); gc.setCompositionMode(QPainter::CompositionMode_Source); drawUsingBackend(gc, info); } } void KisPrescaledProjection::setMonitorProfile(const KoColorProfile *monitorProfile, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) { m_d->projectionBackend->setMonitorProfile(monitorProfile, renderingIntent, conversionFlags); } void KisPrescaledProjection::setChannelFlags(const QBitArray &channelFlags) { m_d->projectionBackend->setChannelFlags(channelFlags); } void KisPrescaledProjection::setDisplayFilter(QSharedPointer displayFilter) { m_d->projectionBackend->setDisplayFilter(displayFilter); } void KisPrescaledProjection::updateViewportSize() { QRectF imageRect = m_d->coordinatesConverter->imageRectInWidgetPixels(); QSizeF minimalSize(qMin(imageRect.width(), (qreal)m_d->canvasSize.width()), qMin(imageRect.height(), (qreal)m_d->canvasSize.height())); QRectF minimalRect(QPointF(0,0), minimalSize); m_d->viewportSize = m_d->coordinatesConverter->widgetToViewport(minimalRect).toAlignedRect().size(); if (m_d->prescaledQImage.isNull() || m_d->prescaledQImage.size() != m_d->viewportSize) { m_d->prescaledQImage = QImage(m_d->viewportSize, QImage::Format_ARGB32); m_d->prescaledQImage.fill(0); } } void KisPrescaledProjection::notifyZoomChanged() { updateViewportSize(); preScale(); } void KisPrescaledProjection::notifyCanvasSizeChanged(const QSize &widgetSize) { m_d->canvasSize = widgetSize; updateViewportSize(); preScale(); } KisPPUpdateInfoSP KisPrescaledProjection::getInitialUpdateInformation(const QRect &dirtyImageRect) { /** * This update information has nothing more than an information * about dirty image rect. All the other information used for * scaling will be fetched in fillUpdateInformation() later, * when we are working in the context of the UI thread */ KisPPUpdateInfoSP info = new KisPPUpdateInfo(); info->dirtyImageRectVar = dirtyImageRect; return info; } void KisPrescaledProjection::fillInUpdateInformation(const QRect &viewportRect, KisPPUpdateInfoSP info) { m_d->coordinatesConverter->imageScale(&info->scaleX, &info->scaleY); // first, crop the part of the view rect that is outside of the canvas QRect croppedViewRect = viewportRect.intersected(QRect(QPoint(0, 0), m_d->viewportSize)); // second, align this rect to the KisImage's pixels and pixels // of projection backend. info->imageRect = m_d->coordinatesConverter->viewportToImage(QRectF(croppedViewRect)).toAlignedRect(); /** * To avoid artifacts while scaling we use mechanism like * changeRect/needRect for layers. Here we grow the rect to update * pixels which depend on the dirty rect (like changeRect), and * later we request a bit more pixels for the patch to make the * scaling safe (like needRect). */ const int borderSize = BORDER_SIZE(qMax(info->scaleX, info->scaleY)); info->imageRect.adjust(-borderSize, -borderSize, borderSize, borderSize); info->imageRect = info->imageRect & m_d->image->bounds(); m_d->projectionBackend->alignSourceRect(info->imageRect, info->scaleX); // finally, compute the dirty rect of the canvas info->viewportRect = m_d->coordinatesConverter->imageToViewport(info->imageRect); info->borderWidth = 0; if (SCALE_MORE_OR_EQUAL_TO(info->scaleX, info->scaleY, 1.0)) { if (SCALE_LESS_THAN(info->scaleX, info->scaleY, 2.0)) { dbgRender << "smoothBetween100And200Percent" << endl; info->renderHints = QPainter::SmoothPixmapTransform; info->borderWidth = borderSize; } info->transfer = KisPPUpdateInfo::DIRECT; } else { // <100% info->renderHints = QPainter::SmoothPixmapTransform; info->borderWidth = borderSize; info->transfer = KisPPUpdateInfo::PATCH; } dbgRender << "#####################################"; dbgRender << ppVar(info->scaleX) << ppVar(info->scaleY); dbgRender << ppVar(info->borderWidth) << ppVar(info->renderHints); dbgRender << ppVar(info->transfer); dbgRender << ppVar(info->dirtyImageRectVar); dbgRender << "Not aligned rect of the canvas (raw):\t" << croppedViewRect; dbgRender << "Update rect in KisImage's pixels:\t" << info->imageRect; dbgRender << "Update rect in canvas' pixels:\t" << info->viewportRect; dbgRender << "#####################################"; } void KisPrescaledProjection::updateScaledImage(KisPPUpdateInfoSP info) { QPainter gc(&m_d->prescaledQImage); gc.setCompositionMode(QPainter::CompositionMode_Source); drawUsingBackend(gc, info); } void KisPrescaledProjection::drawUsingBackend(QPainter &gc, KisPPUpdateInfoSP info) { if (info->imageRect.isEmpty()) return; if (info->transfer == KisPPUpdateInfo::DIRECT) { m_d->projectionBackend->drawFromOriginalImage(gc, info); } else /* if info->transfer == KisPPUpdateInformation::PATCH */ { KisImagePatch patch = m_d->projectionBackend->getNearestPatch(info); // prescale the patch because otherwise we'd scale using QPainter, which gives // a crap result compared to QImage's smoothscale patch.preScale(info->viewportRect); patch.drawMe(gc, info->viewportRect, info->renderHints); } } diff --git a/libs/ui/flake/kis_node_dummies_graph.h b/libs/ui/flake/kis_node_dummies_graph.h index 4c91dcc567..65e2a4f09e 100644 --- a/libs/ui/flake/kis_node_dummies_graph.h +++ b/libs/ui/flake/kis_node_dummies_graph.h @@ -1,146 +1,146 @@ /* * 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. */ #ifndef __KIS_NODE_DUMMIES_GRAPH_H #define __KIS_NODE_DUMMIES_GRAPH_H #include #include #include "kritaui_export.h" #include "kis_types.h" #include "kis_node.h" class KisNodeShape; /** * KisNodeDummy is a simplified representation of a node * in the node stack. It stores all the hierarchy information * about the node, so you needn't access from the node * directly (actually, you cannot do it usually, because UI * works in a different thread and race conditions are possible). * * The dummy stores a KisNodeShape which can store a pointer to * to node. You can access to the node data through it, but take * care -- not all the information is accessible in * multithreading environment. * * The ownership on the KisNodeShape is taken by the dummy. * The ownership on the children of the dummy is taken as well. */ class KRITAUI_EXPORT KisNodeDummy : public QObject { Q_OBJECT public: /** - * Take care tha KisNodeDummy does not take ownership over + * Take care that KisNodeDummy does not take ownership over * the \p nodeShape since the handling of the removal of the * children of the shape is done by flake. So please handle it * manually. * * The children dummies of the dummy are still owned by the * dummy and are deleted automatically. */ KisNodeDummy(KisNodeShape *nodeShape, KisNodeSP node); ~KisNodeDummy() override; KisNodeDummy* firstChild() const; KisNodeDummy* lastChild() const; KisNodeDummy* nextSibling() const; KisNodeDummy* prevSibling() const; KisNodeDummy* parent() const; KisNodeDummy* at(int index) const; int childCount() const; int indexOf(KisNodeDummy *child) const; KisNodeSP node() const; private: friend class KisNodeShapesGraph; // for ::nodeShape() method friend class KisNodeShapesGraphTest; KisNodeShape* nodeShape() const; friend class KisNodeDummiesGraph; QList m_children; KisNodeShape *m_nodeShape; KisNodeSP m_node; }; /** * KisNodeDummiesGraph manages the hierarchy of dummy objects * representing nodes in the UI environment. */ class KRITAUI_EXPORT KisNodeDummiesGraph { public: KisNodeDummiesGraph(); KisNodeDummy* rootDummy() const; KisNodeDummy* nodeToDummy(KisNodeSP node); bool containsNode(KisNodeSP node) const; int dummiesCount() const; /** * Adds a dummy \p node to the position specified * by \p parent and \p aboveThis. * * It is not expected that you would add a dummy twice. */ void addNode(KisNodeDummy *node, KisNodeDummy *parent, KisNodeDummy *aboveThis); /** * Moves a dummy \p node from its current position to * the position specified by \p parent and \p aboveThis. * * It is expected that the dummy \p node has been added * to the graph with addNode() before calling this function. */ void moveNode(KisNodeDummy *node, KisNodeDummy *parent, KisNodeDummy *aboveThis); /** * Removes the dummy \p node from the graph. * * WARNING: The dummy is only "unlinked" from the graph. Neither * deletion of the node nor deletion of its children happens. * The dummy keeps maintaining its children so after unlinking * it from the graph you can just type to free memory recursively: * \code * graph.removeNode(node); * delete node; * \endcode */ void removeNode(KisNodeDummy *node); private: void unmapDummyRecursively(KisNodeDummy *dummy); private: typedef QMap NodeMap; private: KisNodeDummy *m_rootDummy; NodeMap m_dummiesMap; }; #endif /* __KIS_NODE_DUMMIES_GRAPH_H */ diff --git a/libs/ui/input/kis_tool_invocation_action.cpp b/libs/ui/input/kis_tool_invocation_action.cpp index 1fbce7c7ea..be60fbc5f7 100644 --- a/libs/ui/input/kis_tool_invocation_action.cpp +++ b/libs/ui/input/kis_tool_invocation_action.cpp @@ -1,205 +1,205 @@ /* This file is part of the KDE project * Copyright (C) 2012 Arjen Hiemstra * * 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_invocation_action.h" #include #include #include #include #include #include #include "kis_tool.h" #include "kis_input_manager.h" #include "kis_image.h" class KisToolInvocationAction::Private { public: Private() : active(false), lineToolActivated(false) { } bool active; bool lineToolActivated; QPointer activatedToolProxy; QPointer runningToolProxy; }; KisToolInvocationAction::KisToolInvocationAction() : KisAbstractInputAction("Tool Invocation") , d(new Private) { setName(i18n("Tool Invocation")); setDescription(i18n("The Tool Invocation action invokes the current tool, for example, using the brush tool, it will start painting.")); QHash indexes; indexes.insert(i18n("Activate"), ActivateShortcut); indexes.insert(i18n("Confirm"), ConfirmShortcut); indexes.insert(i18n("Cancel"), CancelShortcut); indexes.insert(i18n("Activate Line Tool"), LineToolShortcut); setShortcutIndexes(indexes); } KisToolInvocationAction::~KisToolInvocationAction() { delete d; } void KisToolInvocationAction::activate(int shortcut) { Q_UNUSED(shortcut); if (!inputManager()) return; if (shortcut == LineToolShortcut) { KoToolManager::instance()->switchToolTemporaryRequested("KritaShape/KisToolLine"); d->lineToolActivated = true; } d->activatedToolProxy = inputManager()->toolProxy(); d->activatedToolProxy->activateToolAction(KisTool::Primary); } void KisToolInvocationAction::deactivate(int shortcut) { Q_UNUSED(shortcut); if (!inputManager()) return; /** - * Activate call might ave come before actual input manager or tool proxy - * was attached. So we may end up wil null activatedToolProxy. + * Activate call might have come before actual input manager or tool proxy + * was attached. So we may end up with null activatedToolProxy. */ if (d->activatedToolProxy) { d->activatedToolProxy->deactivateToolAction(KisTool::Primary); d->activatedToolProxy.clear(); } if (shortcut == LineToolShortcut && d->lineToolActivated) { d->lineToolActivated = false; KoToolManager::instance()->switchBackRequested(); } } int KisToolInvocationAction::priority() const { return 0; } bool KisToolInvocationAction::canIgnoreModifiers() const { return true; } void KisToolInvocationAction::begin(int shortcut, QEvent *event) { if (shortcut == ActivateShortcut || shortcut == LineToolShortcut) { d->runningToolProxy = inputManager()->toolProxy(); d->active = d->runningToolProxy->forwardEvent( KisToolProxy::BEGIN, KisTool::Primary, event, event); } else if (shortcut == ConfirmShortcut) { QKeyEvent pressEvent(QEvent::KeyPress, Qt::Key_Return, 0); inputManager()->toolProxy()->keyPressEvent(&pressEvent); QKeyEvent releaseEvent(QEvent::KeyRelease, Qt::Key_Return, 0); inputManager()->toolProxy()->keyReleaseEvent(&releaseEvent); /** * All the tools now have a KisTool::requestStrokeEnd() method, * so they should use this instead of direct filtering Enter key * press. Until all the tools support it, we just duplicate the * key event and the method call */ inputManager()->canvas()->image()->requestStrokeEnd(); /** * Some tools would like to distinguish automated requestStrokeEnd() * calls from explicit user actions. Just let them do it! * * Please note that this call should happen **after** * requestStrokeEnd(). Some of the tools will switch to another * tool on this request, and this (next) tool does not expect to * get requestStrokeEnd() right after switching in. */ inputManager()->toolProxy()->explicitUserStrokeEndRequest(); } else if (shortcut == CancelShortcut) { /** * The tools now have a KisTool::requestStrokeCancellation() method, * so just request it. */ inputManager()->canvas()->image()->requestStrokeCancellation(); } } void KisToolInvocationAction::end(QEvent *event) { if (d->active) { // It might happen that the action is still running, while the // canvas has been removed, which kills the toolProxy. KIS_SAFE_ASSERT_RECOVER_NOOP(d->runningToolProxy); if (d->runningToolProxy) { d->runningToolProxy-> forwardEvent(KisToolProxy::END, KisTool::Primary, event, event); d->runningToolProxy.clear(); } d->active = false; } KisAbstractInputAction::end(event); } void KisToolInvocationAction::inputEvent(QEvent* event) { if (!d->active) return; if (!d->runningToolProxy) return; d->runningToolProxy-> forwardEvent(KisToolProxy::CONTINUE, KisTool::Primary, event, event); } void KisToolInvocationAction::processUnhandledEvent(QEvent* event) { bool savedState = d->active; KisToolProxy *savedToolProxy = d->runningToolProxy; if (!d->runningToolProxy) { d->runningToolProxy = inputManager()->toolProxy(); } d->active = true; inputEvent(event); d->active = savedState; d->runningToolProxy = savedToolProxy; } bool KisToolInvocationAction::supportsHiResInputEvents() const { return inputManager()->toolProxy()->primaryActionSupportsHiResEvents(); } bool KisToolInvocationAction::isShortcutRequired(int shortcut) const { //These really all are pretty important for basic user interaction. Q_UNUSED(shortcut) return true; } diff --git a/libs/ui/kis_node_model.h b/libs/ui/kis_node_model.h index 52d8df1662..9237ad6243 100644 --- a/libs/ui/kis_node_model.h +++ b/libs/ui/kis_node_model.h @@ -1,195 +1,195 @@ /* * Copyright (c) 2007 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_NODE_MODEL #define KIS_NODE_MODEL #include "kritaui_export.h" #include #include #include #include #include #include #include class KisDummiesFacadeBase; class KisNodeDummy; class KisShapeController; class KisModelIndexConverterBase; class KisNodeSelectionAdapter; class KisNodeInsertionAdapter; class KisSelectionActionsAdapter; class KisNodeDisplayModeAdapter; class KisNodeManager; /** * KisNodeModel offers a Qt model-view compatible view of the node * hierarchy. The KisNodeView displays a thumbnail and a row of * icon properties for every document section. * * Note that there's a discrepancy between the krita node tree model * and the model Qt wants to see: we hide the root node from Qt. * * The node model also shows an inverse view of the layer tree: we want * the first layer to show up at the bottom. * * See also the Qt documentation for QAbstractItemModel. * This class extends that interface to provide a name and set of toggle * properties (like visible, locked, selected.) * */ class KRITAUI_EXPORT KisNodeModel : public QAbstractItemModel { Q_OBJECT public: /// Extensions to Qt::ItemDataRole. enum ItemDataRole { /// Whether the section is the active one ActiveRole = Qt::UserRole + 1, /// A list of properties the part has. PropertiesRole, /// The aspect ratio of the section as a floating point value: width divided by height. AspectRatioRole, /// Use to communicate a progress report to the section delegate on an action (a value of -1 or a QVariant() disable the progress bar ProgressRole, - /// Speacial activation role which is emitted when the user Atl-clicks on a section + /// Special activation role which is emitted when the user Atl-clicks on a section /// The item is first activated with ActiveRole, then a separate AlternateActiveRole comes AlternateActiveRole, // When a layer is not (recursively) visible, then it should be gayed out ShouldGrayOutRole, // An index of a color label associated with the node ColorLabelIndexRole, // Instruct this model to update all its items' Qt::ItemIsDropEnabled flags in order to // reflect if the item allows an "onto" drop of the given QMimeData*. DropEnabled, // Instructs the model to activate "select opaque" action, // the selection action (of type SelectionAction) value // is passed via QVariant as integer SelectOpaqueRole, // Returns a text explaining why the node has been excluded from // projection rendering. If the node is not excluded, then empty // string is returned DropReasonRole, /// This is to ensure that we can extend the data role in the future, since it's not possible to add a role after BeginThumbnailRole (due to "Hack") ReservedRole = Qt::UserRole + 99, /** * For values of BeginThumbnailRole or higher, a thumbnail of the layer of which neither dimension * is larger than (int) value - (int) BeginThumbnailRole. * This is a hack to work around the fact that Interview doesn't have a nice way to * request thumbnails of arbitrary size. */ BeginThumbnailRole }; public: // from QAbstractItemModel KisNodeModel(QObject * parent); ~KisNodeModel() override; void setDummiesFacade(KisDummiesFacadeBase *dummiesFacade, KisImageWSP image, KisShapeController *shapeController, KisSelectionActionsAdapter *selectionActionsAdapter, KisNodeManager *nodeManager); KisNodeSP nodeFromIndex(const QModelIndex &index) const; QModelIndex indexFromNode(KisNodeSP node) const; bool showGlobalSelection() const; public Q_SLOTS: void setShowGlobalSelection(bool value); public: int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; QModelIndex parent(const QModelIndex &index) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; Qt::ItemFlags flags(const QModelIndex &index) const override; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; QStringList mimeTypes() const override; QMimeData* mimeData(const QModelIndexList & indexes) const override; bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) override; bool canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const override; Qt::DropActions supportedDragActions() const override; Qt::DropActions supportedDropActions() const override; bool hasDummiesFacade(); static bool belongsToIsolatedGroup(KisImageSP image, KisNodeSP node, KisDummiesFacadeBase *dummiesFacade); Q_SIGNALS: void toggleIsolateActiveNode(); protected Q_SLOTS: void slotBeginInsertDummy(KisNodeDummy *parent, int index, const QString &metaObjectType); void slotEndInsertDummy(KisNodeDummy *dummy); void slotBeginRemoveDummy(KisNodeDummy *dummy); void slotEndRemoveDummy(); void slotDummyChanged(KisNodeDummy *dummy); void slotIsolatedModeChanged(); void slotNodeDisplayModeChanged(bool showRootNode, bool showGlobalSelectionMask); void processUpdateQueue(); void progressPercentageChanged(int, const KisNodeSP); protected: virtual KisModelIndexConverterBase *createIndexConverter(); KisModelIndexConverterBase *indexConverter() const; KisDummiesFacadeBase *dummiesFacade() const; private: friend class KisModelIndexConverter; friend class KisModelIndexConverterShowAll; void connectDummy(KisNodeDummy *dummy, bool needConnect); void connectDummies(KisNodeDummy *dummy, bool needConnect); void resetIndexConverter(); void regenerateItems(KisNodeDummy *dummy); bool belongsToIsolatedGroup(KisNodeSP node) const; void setDropEnabled(const QMimeData *data); void updateDropEnabled(const QList &nodes, QModelIndex parent = QModelIndex()); private: struct Private; Private * const m_d; }; #endif diff --git a/libs/ui/kis_painting_assistant.h b/libs/ui/kis_painting_assistant.h index 1a8b58eff1..e6a4c8c2e5 100644 --- a/libs/ui/kis_painting_assistant.h +++ b/libs/ui/kis_painting_assistant.h @@ -1,250 +1,250 @@ /* * Copyright (c) 2008 Cyrille Berger * 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_PAINTING_ASSISTANT_H_ #define _KIS_PAINTING_ASSISTANT_H_ #include #include #include #include #include #include #include #include #include #include #include class QPainter; class QRect; class QRectF; class KoStore; class KisCoordinatesConverter; class KisCanvas2; class QDomDocument; class QDomElement; #include #include class KisPaintingAssistantHandle; typedef KisSharedPtr KisPaintingAssistantHandleSP; class KisPaintingAssistant; class QPainterPath; enum HandleType { NORMAL, SIDE, CORNER, VANISHING_POINT, ANCHOR }; /** * Represent an handle of the assistant, used to edit the parameters * of an assistants. Handles can be shared between assistants. */ class KRITAUI_EXPORT KisPaintingAssistantHandle : public QPointF, public KisShared { friend class KisPaintingAssistant; public: KisPaintingAssistantHandle(double x, double y); explicit KisPaintingAssistantHandle(QPointF p); KisPaintingAssistantHandle(const KisPaintingAssistantHandle&); ~KisPaintingAssistantHandle(); void mergeWith(KisPaintingAssistantHandleSP); void uncache(); KisPaintingAssistantHandle& operator=(const QPointF&); void setType(char type); char handleType() const; /** * Returns the pointer to the "chief" assistant, * which is supposed to handle transformations of the * handle, when all the assistants are transformed */ KisPaintingAssistant* chiefAssistant() const; private: void registerAssistant(KisPaintingAssistant*); void unregisterAssistant(KisPaintingAssistant*); bool containsAssistant(KisPaintingAssistant*) const; private: struct Private; Private* const d; }; /** * A KisPaintingAssistant is an object that assist the drawing on the canvas. * With this class you can implement virtual equivalent to ruler or compas. */ class KRITAUI_EXPORT KisPaintingAssistant { public: KisPaintingAssistant(const QString& id, const QString& name); virtual ~KisPaintingAssistant(); virtual KisPaintingAssistantSP clone(QMap &handleMap) const = 0; const QString& id() const; const QString& name() const; bool isSnappingActive() const; void setSnappingActive(bool set); /** * Adjust the position given in parameter. * @param point the coordinates in point in the document reference * @param strokeBegin the coordinates of the beginning of the stroke */ virtual QPointF adjustPosition(const QPointF& point, const QPointF& strokeBegin) = 0; virtual void endStroke() { } virtual QPointF getEditorPosition() const = 0; // Returns editor widget position in document-space coordinates. virtual int numHandles() const = 0; void replaceHandle(KisPaintingAssistantHandleSP _handle, KisPaintingAssistantHandleSP _with); void addHandle(KisPaintingAssistantHandleSP handle, HandleType type); QPointF viewportConstrainedEditorPosition(const KisCoordinatesConverter* converter, const QSize editorSize); QColor effectiveAssistantColor() const; bool useCustomColor(); void setUseCustomColor(bool useCustomColor); void setAssistantCustomColor(QColor color); QColor assistantCustomColor(); void setAssistantGlobalColorCache(const QColor &color); virtual void drawAssistant(QPainter& gc, const QRectF& updateRect, const KisCoordinatesConverter *converter, bool cached = true,KisCanvas2 *canvas=0, bool assistantVisible=true, bool previewVisible=true); void uncache(); const QList& handles() const; QList handles(); const QList& sideHandles() const; QList sideHandles(); QByteArray saveXml( QMap &handleMap); virtual void saveCustomXml(QXmlStreamWriter* xml); //in case specific assistants have custom properties (like vanishing point) void loadXml(KoStore *store, QMap &handleMap, QString path); virtual bool loadCustomXml(QXmlStreamReader* xml); void saveXmlList(QDomDocument& doc, QDomElement& ssistantsElement, int count); void findPerspectiveAssistantHandleLocation(); KisPaintingAssistantHandleSP oppHandleOne(); /** * Get the topLeft, bottomLeft, topRight and BottomRight corners of the assistant * Some assistants like the perspective grid have custom logic built around certain handles */ const KisPaintingAssistantHandleSP topLeft() const; KisPaintingAssistantHandleSP topLeft(); const KisPaintingAssistantHandleSP topRight() const; KisPaintingAssistantHandleSP topRight(); const KisPaintingAssistantHandleSP bottomLeft() const; KisPaintingAssistantHandleSP bottomLeft(); const KisPaintingAssistantHandleSP bottomRight() const; KisPaintingAssistantHandleSP bottomRight(); const KisPaintingAssistantHandleSP topMiddle() const; KisPaintingAssistantHandleSP topMiddle(); const KisPaintingAssistantHandleSP rightMiddle() const; KisPaintingAssistantHandleSP rightMiddle(); const KisPaintingAssistantHandleSP leftMiddle() const; KisPaintingAssistantHandleSP leftMiddle(); const KisPaintingAssistantHandleSP bottomMiddle() const; KisPaintingAssistantHandleSP bottomMiddle(); // calculates whether a point is near one of the corner points of the assistant // returns: a corner point from the perspective assistant if the given node is close // only called once in code when calculating the perspective assistant KisPaintingAssistantHandleSP closestCornerHandleFromPoint(QPointF point); // determines if two points are close to each other // only used by the nodeNearPoint function (perspective grid assistant). bool areTwoPointsClose(const QPointF& pointOne, const QPointF& pointTwo); /// determines if the assistant has enough handles to be considered created /// new assistants get in a "creation" phase where they are currently being made on the canvas /// it will return false if we are in the middle of creating the assistant. virtual bool isAssistantComplete() const; - /// Transform the assistant using the given \p tranform. Please note that \p transform + /// Transform the assistant using the given \p transform. Please note that \p transform /// should be in 'document' coordinate system. /// Used with image-wide transformations. virtual void transform(const QTransform &transform); public: /** * This will render the final output. The drawCache does rendering most of the time so be sure to check that */ void drawPath(QPainter& painter, const QPainterPath& path, bool drawActive=true); void drawPreview(QPainter& painter, const QPainterPath& path); static double norm2(const QPointF& p); protected: explicit KisPaintingAssistant(const KisPaintingAssistant &rhs, QMap &handleMap); virtual QRect boundingRect() const; /// performance layer where the graphics can be drawn from a cache instead of generated every render update virtual void drawCache(QPainter& gc, const KisCoordinatesConverter *converter, bool assistantVisible=true) = 0; void initHandles(QList _handles); QList m_handles; QPointF pixelToView(const QPoint pixelCoords) const; public: /// clones the list of assistants /// the originally shared handles will still be shared /// the cloned assistants do not share any handle with the original assistants static QList cloneAssistantList(const QList &list); private: struct Private; Private* const d; }; /** * Allow to create a painting assistant. */ class KRITAUI_EXPORT KisPaintingAssistantFactory { public: KisPaintingAssistantFactory(); virtual ~KisPaintingAssistantFactory(); virtual QString id() const = 0; virtual QString name() const = 0; virtual KisPaintingAssistant* createPaintingAssistant() const = 0; }; class KRITAUI_EXPORT KisPaintingAssistantFactoryRegistry : public KoGenericRegistry { public: KisPaintingAssistantFactoryRegistry(); ~KisPaintingAssistantFactoryRegistry() override; static KisPaintingAssistantFactoryRegistry* instance(); }; #endif diff --git a/libs/ui/kis_paintop_option.h b/libs/ui/kis_paintop_option.h index e04d32bea7..c25147e96e 100644 --- a/libs/ui/kis_paintop_option.h +++ b/libs/ui/kis_paintop_option.h @@ -1,133 +1,133 @@ /* This file is part of the KDE project * Copyright (C) Boudewijn Rempt , (C) 2008 * * 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_PAINTOP_OPTION_H #define KIS_PAINTOP_OPTION_H #include #include #include #include #include class QWidget; class QString; class KisPaintopLodLimitations; /** * Base interface for paintop options. A paintop option * can be enabled/disabled, has a configuration page * (for example, a curve), a user-visible name and can * be serialized and deserialized into KisPaintOpPresets * * Because KisPaintOpOption classes create a QWidget in * their constructor (the configuration page) you CANNOT * create those objects in a KisPaintOp. KisPaintOps are * created in non-gui threads. * * Options are disabled by default. */ class KRITAUI_EXPORT KisPaintOpOption : public QObject { Q_OBJECT public: enum PaintopCategory { GENERAL, COLOR, TEXTURE, FILTER, MASKING_BRUSH }; KisPaintOpOption(KisPaintOpOption::PaintopCategory category, bool checked); ~KisPaintOpOption() override; KisPaintOpOption::PaintopCategory category() const; virtual bool isCheckable() const; virtual bool isChecked() const; virtual void setChecked(bool checked); void setLocked(bool value); bool isLocked() const; /** * Reimplement this to use the image in the option widget */ virtual void setImage(KisImageWSP image); virtual void setNode(KisNodeWSP node); void startReadOptionSetting(const KisPropertiesConfigurationSP setting); void startWriteOptionSetting(KisPropertiesConfigurationSP setting) const; QWidget *configurationPage() const; virtual void lodLimitations(KisPaintopLodLimitations *l) const; protected: void setConfigurationPage(QWidget *page); protected: /** * Re-implement this to save the configuration to the paint configuration. */ virtual void writeOptionSetting(KisPropertiesConfigurationSP setting) const { Q_UNUSED(setting); } /** - * Re-implement this to set te widgets with the values in @p setting. + * Re-implement this to set the widgets with the values in @p setting. */ virtual void readOptionSetting(const KisPropertiesConfigurationSP setting) { Q_UNUSED(setting); } protected Q_SLOTS: void emitSettingChanged(); void emitCheckedChanged(); Q_SIGNALS: /** * emit this whenever a setting has changed. It will update the preview */ void sigSettingChanged(); /** * emit this whenever a checked state of the option has changed. It as always * emitted *before* sigSettingChanged() */ void sigCheckedChanged(bool value); protected: bool m_checkable; bool m_locked; private: struct Private; Private* const m_d; }; #endif diff --git a/libs/ui/kis_safe_document_loader.cpp b/libs/ui/kis_safe_document_loader.cpp index 488d2cd426..343c1d4ae8 100644 --- a/libs/ui/kis_safe_document_loader.cpp +++ b/libs/ui/kis_safe_document_loader.cpp @@ -1,275 +1,275 @@ /* * 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_safe_document_loader.h" #include #include #include #include #include #include #include #include #include #include "KisDocument.h" #include #include "kis_signal_compressor.h" #include "KisPart.h" class FileSystemWatcherWrapper : public QObject { Q_OBJECT public: FileSystemWatcherWrapper() { connect(&m_watcher, SIGNAL(fileChanged(QString)), SIGNAL(fileChanged(QString))); connect(&m_watcher, SIGNAL(fileChanged(QString)), SLOT(slotFileChanged(QString))); } bool addPath(const QString &file) { bool result = true; const QString ufile = unifyFilePath(file); if (m_pathCount.contains(ufile)) { m_pathCount[ufile]++; } else { m_pathCount.insert(ufile, 1); result = m_watcher.addPath(ufile); } return result; } bool removePath(const QString &file) { bool result = true; const QString ufile = unifyFilePath(file); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(m_pathCount.contains(ufile), false); if (m_pathCount[ufile] == 1) { m_pathCount.remove(ufile); result = m_watcher.removePath(ufile); } else { m_pathCount[ufile]--; } return result; } QStringList files() const { return m_watcher.files(); } private Q_SLOTS: void slotFileChanged(const QString &path) { // re-add the file after QSaveFile optimization if (!m_watcher.files().contains(path) && QFileInfo(path).exists()) { m_watcher.addPath(path); } } Q_SIGNALS: void fileChanged(const QString &path); private: QString unifyFilePath(const QString &path) { return QFileInfo(path).absoluteFilePath(); } private: QFileSystemWatcher m_watcher; QHash m_pathCount; }; Q_GLOBAL_STATIC(FileSystemWatcherWrapper, s_fileSystemWatcher) struct KisSafeDocumentLoader::Private { Private() : fileChangedSignalCompressor(500 /* ms */, KisSignalCompressor::POSTPONE) { } QScopedPointer doc; KisSignalCompressor fileChangedSignalCompressor; bool isLoading = false; bool fileChangedFlag = false; QString path; QString temporaryPath; qint64 initialFileSize = 0; QDateTime initialFileTimeStamp; }; KisSafeDocumentLoader::KisSafeDocumentLoader(const QString &path, QObject *parent) : QObject(parent), m_d(new Private()) { connect(s_fileSystemWatcher, SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString))); connect(&m_d->fileChangedSignalCompressor, SIGNAL(timeout()), SLOT(fileChangedCompressed())); setPath(path); } KisSafeDocumentLoader::~KisSafeDocumentLoader() { if (!m_d->path.isEmpty()) { s_fileSystemWatcher->removePath(m_d->path); } delete m_d; } void KisSafeDocumentLoader::setPath(const QString &path) { if (path.isEmpty()) return; if (!m_d->path.isEmpty()) { s_fileSystemWatcher->removePath(m_d->path); } m_d->path = path; s_fileSystemWatcher->addPath(m_d->path); } void KisSafeDocumentLoader::reloadImage() { fileChangedCompressed(true); } void KisSafeDocumentLoader::fileChanged(QString path) { if (path == m_d->path) { m_d->fileChangedFlag = true; m_d->fileChangedSignalCompressor.start(); } } void KisSafeDocumentLoader::fileChangedCompressed(bool sync) { if (m_d->isLoading) return; QFileInfo initialFileInfo(m_d->path); m_d->initialFileSize = initialFileInfo.size(); m_d->initialFileTimeStamp = initialFileInfo.lastModified(); if (s_fileSystemWatcher->files().contains(m_d->path) == false && initialFileInfo.exists()) { - //When a path is renamed it is removed, so we ought to readd it. + //When a path is renamed it is removed, so we ought to re-add it. s_fileSystemWatcher->addPath(m_d->path); } // it may happen when the file is flushed by // so other application if (!m_d->initialFileSize) return; m_d->isLoading = true; m_d->fileChangedFlag = false; m_d->temporaryPath = QDir::tempPath() + QDir::separator() + QString("krita_file_layer_copy_%1_%2.%3") .arg(QApplication::applicationPid()) .arg(qrand()) .arg(initialFileInfo.suffix()); QFile::copy(m_d->path, m_d->temporaryPath); if (!sync) { QTimer::singleShot(100, this, SLOT(delayedLoadStart())); } else { QApplication::processEvents(); delayedLoadStart(); } } void KisSafeDocumentLoader::delayedLoadStart() { QFileInfo originalInfo(m_d->path); QFileInfo tempInfo(m_d->temporaryPath); bool successfullyLoaded = false; if (!m_d->fileChangedFlag && originalInfo.size() == m_d->initialFileSize && originalInfo.lastModified() == m_d->initialFileTimeStamp && tempInfo.size() == m_d->initialFileSize) { m_d->doc.reset(KisPart::instance()->createDocument()); if (m_d->path.toLower().endsWith("ora") || m_d->path.toLower().endsWith("kra")) { QScopedPointer store(KoStore::createStore(m_d->temporaryPath, KoStore::Read)); if (store && !store->bad()) { if (store->open(QString("mergedimage.png"))) { QByteArray bytes = store->read(store->size()); store->close(); QImage mergedImage; mergedImage.loadFromData(bytes); Q_ASSERT(!mergedImage.isNull()); KisImageSP image = new KisImage(0, mergedImage.width(), mergedImage.height(), KoColorSpaceRegistry::instance()->rgb8(), ""); KisPaintLayerSP layer = new KisPaintLayer(image, "", OPACITY_OPAQUE_U8); layer->paintDevice()->convertFromQImage(mergedImage, 0); image->addNode(layer, image->rootLayer()); image->initialRefreshGraph(); m_d->doc->setCurrentImage(image); successfullyLoaded = true; } else { qWarning() << "delayedLoadStart: Could not open mergedimage.png"; } } else { qWarning() << "delayedLoadStart: Store was bad"; } } else { successfullyLoaded = m_d->doc->openUrl(QUrl::fromLocalFile(m_d->temporaryPath), KisDocument::DontAddToRecent); } } else { dbgKrita << "File was modified externally. Restarting."; dbgKrita << ppVar(m_d->fileChangedFlag); dbgKrita << ppVar(m_d->initialFileSize); dbgKrita << ppVar(m_d->initialFileTimeStamp); dbgKrita << ppVar(originalInfo.size()); dbgKrita << ppVar(originalInfo.lastModified()); dbgKrita << ppVar(tempInfo.size()); } QFile::remove(m_d->temporaryPath); m_d->isLoading = false; if (!successfullyLoaded) { // Restart the attempt m_d->fileChangedSignalCompressor.start(); } else { KisPaintDeviceSP paintDevice = new KisPaintDevice(m_d->doc->image()->colorSpace()); KisPaintDeviceSP projection = m_d->doc->image()->projection(); paintDevice->makeCloneFrom(projection, projection->extent()); emit loadingFinished(paintDevice, m_d->doc->image()->xRes(), m_d->doc->image()->yRes()); } m_d->doc.reset(); } #include "kis_safe_document_loader.moc" diff --git a/libs/ui/opengl/KisOpenGLUpdateInfoBuilder.cpp b/libs/ui/opengl/KisOpenGLUpdateInfoBuilder.cpp index c34367cbd0..07b1a4081e 100644 --- a/libs/ui/opengl/KisOpenGLUpdateInfoBuilder.cpp +++ b/libs/ui/opengl/KisOpenGLUpdateInfoBuilder.cpp @@ -1,272 +1,272 @@ /* * Copyright (c) 2018 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 "KisOpenGLUpdateInfoBuilder.h" // TODO: conversion options into a separate file! #include "kis_update_info.h" #include "opengl/kis_texture_tile_info_pool.h" #include "KisProofingConfiguration.h" #include #include #include struct KRITAUI_NO_EXPORT KisOpenGLUpdateInfoBuilder::Private { ConversionOptions conversionOptions; QBitArray channelFlags; bool onlyOneChannelSelected = false; int selectedChannelIndex = -1; int textureBorder = 0; QSize effectiveTextureSize; KisProofingConfigurationSP proofingConfig; QScopedPointer proofingTransform; KisTextureTileInfoPoolSP pool; QReadWriteLock lock; }; KisOpenGLUpdateInfoBuilder::KisOpenGLUpdateInfoBuilder() : m_d(new Private) { } KisOpenGLUpdateInfoBuilder::~KisOpenGLUpdateInfoBuilder() { } KisOpenGLUpdateInfoSP KisOpenGLUpdateInfoBuilder::buildUpdateInfo(const QRect &rect, KisImageSP srcImage, bool convertColorSpace) { return buildUpdateInfo(rect, srcImage->projection(), srcImage->bounds(), srcImage->currentLevelOfDetail(), convertColorSpace); } KisOpenGLUpdateInfoSP KisOpenGLUpdateInfoBuilder::buildUpdateInfo(const QRect &rect, KisPaintDeviceSP projection, const QRect &bounds, int levelOfDetail, bool convertColorSpace) { KisOpenGLUpdateInfoSP info = new KisOpenGLUpdateInfo(); QRect updateRect = rect & bounds; if (updateRect.isEmpty()) return info; KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(m_d->pool, info); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(m_d->conversionOptions.m_destinationColorSpace, info); auto needCreateProofingTransform = [this] () { return !m_d->proofingTransform && m_d->proofingConfig && m_d->proofingConfig->conversionFlags.testFlag(KoColorConversionTransformation::SoftProofing); }; // lazily create transform if (convertColorSpace && needCreateProofingTransform()) { QWriteLocker locker(&m_d->lock); if (needCreateProofingTransform()) { const KoColorSpace *proofingSpace = KoColorSpaceRegistry::instance()->colorSpace(m_d->proofingConfig->proofingModel, m_d->proofingConfig->proofingDepth, m_d->proofingConfig->proofingProfile); m_d->proofingTransform.reset(KisTextureTileUpdateInfo::generateProofingTransform( projection->colorSpace(), m_d->conversionOptions.m_destinationColorSpace, proofingSpace, m_d->conversionOptions.m_renderingIntent, m_d->proofingConfig->intent, m_d->proofingConfig->conversionFlags, m_d->proofingConfig->warningColor, m_d->proofingConfig->adaptationState)); } } QReadLocker locker(&m_d->lock); /** * Why the rect is artificial? That's easy! * It does not represent any real piece of the image. It is - * intentionally stretched to get through the overlappping + * intentionally stretched to get through the overlapping * stripes of neutrality and poke neighbouring tiles. * Thanks to the rect we get the coordinates of all the tiles * involved into update process */ QRect artificialRect = kisGrowRect(updateRect, m_d->textureBorder); artificialRect &= bounds; int firstColumn = xToCol(artificialRect.left()); int lastColumn = xToCol(artificialRect.right()); int firstRow = yToRow(artificialRect.top()); int lastRow = yToRow(artificialRect.bottom()); QBitArray channelFlags; // empty by default if (!m_d->channelFlags.isEmpty() && m_d->channelFlags.size() == projection->colorSpace()->channels().size()) { channelFlags = m_d->channelFlags; } qint32 numItems = (lastColumn - firstColumn + 1) * (lastRow - firstRow + 1); info->tileList.reserve(numItems); QRect alignedUpdateRect = updateRect; QRect alignedBounds = bounds; if (levelOfDetail) { alignedUpdateRect = KisLodTransform::alignedRect(alignedUpdateRect, levelOfDetail); alignedBounds = KisLodTransform::alignedRect(alignedBounds, levelOfDetail); } for (int col = firstColumn; col <= lastColumn; col++) { for (int row = firstRow; row <= lastRow; row++) { const QRect alignedTileTextureRect = calculatePhysicalTileRect(col, row, bounds, levelOfDetail); KisTextureTileUpdateInfoSP tileInfo( new KisTextureTileUpdateInfo(col, row, alignedTileTextureRect, alignedUpdateRect, alignedBounds, levelOfDetail, m_d->pool)); // Don't update empty tiles if (tileInfo->valid()) { tileInfo->retrieveData(projection, channelFlags, m_d->onlyOneChannelSelected, m_d->selectedChannelIndex); if (convertColorSpace) { if (m_d->proofingTransform) { tileInfo->proofTo(m_d->conversionOptions.m_destinationColorSpace, m_d->proofingConfig->conversionFlags, m_d->proofingTransform.data()); } else { tileInfo->convertTo(m_d->conversionOptions.m_destinationColorSpace, m_d->conversionOptions.m_renderingIntent, m_d->conversionOptions.m_conversionFlags); } } info->tileList.append(tileInfo); } else { dbgUI << "Trying to create an empty tileinfo record" << col << row << alignedTileTextureRect << updateRect << bounds; } } } info->assignDirtyImageRect(rect); info->assignLevelOfDetail(levelOfDetail); return info; } QRect KisOpenGLUpdateInfoBuilder::calculateEffectiveTileRect(int col, int row, const QRect &imageBounds) const { return imageBounds & QRect(col * m_d->effectiveTextureSize.width(), row * m_d->effectiveTextureSize.height(), m_d->effectiveTextureSize.width(), m_d->effectiveTextureSize.height()); } QRect KisOpenGLUpdateInfoBuilder::calculatePhysicalTileRect(int col, int row, const QRect &imageBounds, int levelOfDetail) const { const QRect tileRect = calculateEffectiveTileRect(col, row, imageBounds); const QRect tileTextureRect = kisGrowRect(tileRect, m_d->textureBorder); const QRect alignedTileTextureRect = levelOfDetail ? KisLodTransform::alignedRect(tileTextureRect, levelOfDetail) : tileTextureRect; return alignedTileTextureRect; } int KisOpenGLUpdateInfoBuilder::xToCol(int x) const { return x / m_d->effectiveTextureSize.width(); } int KisOpenGLUpdateInfoBuilder::yToRow(int y) const { return y / m_d->effectiveTextureSize.height(); } const KoColorSpace *KisOpenGLUpdateInfoBuilder::destinationColorSpace() const { QReadLocker lock(&m_d->lock); return m_d->conversionOptions.m_destinationColorSpace; } void KisOpenGLUpdateInfoBuilder::setConversionOptions(const ConversionOptions &options) { QWriteLocker lock(&m_d->lock); m_d->conversionOptions = options; } void KisOpenGLUpdateInfoBuilder::setChannelFlags(const QBitArray &channelFrags, bool onlyOneChannelSelected, int selectedChannelIndex) { QWriteLocker lock(&m_d->lock); m_d->channelFlags = channelFrags; m_d->onlyOneChannelSelected = onlyOneChannelSelected; m_d->selectedChannelIndex = selectedChannelIndex; } void KisOpenGLUpdateInfoBuilder::setTextureBorder(int value) { QWriteLocker lock(&m_d->lock); m_d->textureBorder = value; } void KisOpenGLUpdateInfoBuilder::setEffectiveTextureSize(const QSize &size) { QWriteLocker lock(&m_d->lock); m_d->effectiveTextureSize = size; } void KisOpenGLUpdateInfoBuilder::setTextureInfoPool(KisTextureTileInfoPoolSP pool) { QWriteLocker lock(&m_d->lock); m_d->pool = pool; } KisTextureTileInfoPoolSP KisOpenGLUpdateInfoBuilder::textureInfoPool() const { QReadLocker lock(&m_d->lock); return m_d->pool; } void KisOpenGLUpdateInfoBuilder::setProofingConfig(KisProofingConfigurationSP config) { QWriteLocker lock(&m_d->lock); m_d->proofingConfig = config; m_d->proofingTransform.reset(); } KisProofingConfigurationSP KisOpenGLUpdateInfoBuilder::proofingConfig() const { QReadLocker lock(&m_d->lock); return m_d->proofingConfig; } diff --git a/libs/ui/opengl/kis_opengl_canvas2.cpp b/libs/ui/opengl/kis_opengl_canvas2.cpp index ef2617b273..b9d090d11c 100644 --- a/libs/ui/opengl/kis_opengl_canvas2.cpp +++ b/libs/ui/opengl/kis_opengl_canvas2.cpp @@ -1,1061 +1,1061 @@ /* This file is part of the KDE project * Copyright (C) Boudewijn Rempt , (C) 2006-2013 * Copyright (C) 2015 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. */ #define GL_GLEXT_PROTOTYPES #include "opengl/kis_opengl_canvas2.h" #include "opengl/kis_opengl_canvas2_p.h" #include "opengl/kis_opengl_shader_loader.h" #include "opengl/kis_opengl_canvas_debugger.h" #include "canvas/kis_canvas2.h" #include "canvas/kis_coordinates_converter.h" #include "canvas/kis_display_filter.h" #include "canvas/kis_display_color_converter.h" #include "kis_config.h" #include "kis_config_notifier.h" #include "kis_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include "KisOpenGLModeProber.h" #include #if !defined(Q_OS_MACOS) && !defined(HAS_ONLY_OPENGL_ES) #include #endif #define NEAR_VAL -1000.0 #define FAR_VAL 1000.0 #ifndef GL_CLAMP_TO_EDGE #define GL_CLAMP_TO_EDGE 0x812F #endif #define PROGRAM_VERTEX_ATTRIBUTE 0 #define PROGRAM_TEXCOORD_ATTRIBUTE 1 static bool OPENGL_SUCCESS = false; struct KisOpenGLCanvas2::Private { public: ~Private() { delete displayShader; delete checkerShader; delete solidColorShader; Sync::deleteSync(glSyncObject); } bool canvasInitialized{false}; KisOpenGLImageTexturesSP openGLImageTextures; KisOpenGLShaderLoader shaderLoader; KisShaderProgram *displayShader{0}; KisShaderProgram *checkerShader{0}; KisShaderProgram *solidColorShader{0}; bool displayShaderCompiledWithDisplayFilterSupport{false}; GLfloat checkSizeScale; bool scrollCheckers; QSharedPointer displayFilter; KisOpenGL::FilterMode filterMode; bool proofingConfigIsUpdated=false; GLsync glSyncObject{0}; bool wrapAroundMode{false}; // Stores a quad for drawing the canvas QOpenGLVertexArrayObject quadVAO; QOpenGLBuffer quadBuffers[2]; // Stores data for drawing tool outlines QOpenGLVertexArrayObject outlineVAO; QOpenGLBuffer lineBuffer; QVector3D vertices[6]; QVector2D texCoords[6]; #if !defined(Q_OS_MACOS) && !defined(HAS_ONLY_OPENGL_ES) QOpenGLFunctions_2_1 *glFn201; #endif qreal pixelGridDrawingThreshold; bool pixelGridEnabled; QColor gridColor; QColor cursorColor; bool lodSwitchInProgress = false; int xToColWithWrapCompensation(int x, const QRect &imageRect) { int firstImageColumn = openGLImageTextures->xToCol(imageRect.left()); int lastImageColumn = openGLImageTextures->xToCol(imageRect.right()); int colsPerImage = lastImageColumn - firstImageColumn + 1; int numWraps = floor(qreal(x) / imageRect.width()); int remainder = x - imageRect.width() * numWraps; return colsPerImage * numWraps + openGLImageTextures->xToCol(remainder); } int yToRowWithWrapCompensation(int y, const QRect &imageRect) { int firstImageRow = openGLImageTextures->yToRow(imageRect.top()); int lastImageRow = openGLImageTextures->yToRow(imageRect.bottom()); int rowsPerImage = lastImageRow - firstImageRow + 1; int numWraps = floor(qreal(y) / imageRect.height()); int remainder = y - imageRect.height() * numWraps; return rowsPerImage * numWraps + openGLImageTextures->yToRow(remainder); } }; KisOpenGLCanvas2::KisOpenGLCanvas2(KisCanvas2 *canvas, KisCoordinatesConverter *coordinatesConverter, QWidget *parent, KisImageWSP image, KisDisplayColorConverter *colorConverter) : QOpenGLWidget(parent) , KisCanvasWidgetBase(canvas, coordinatesConverter) , d(new Private()) { KisConfig cfg(false); cfg.setCanvasState("OPENGL_STARTED"); d->openGLImageTextures = KisOpenGLImageTextures::getImageTextures(image, colorConverter->openGLCanvasSurfaceProfile(), colorConverter->renderingIntent(), colorConverter->conversionFlags()); connect(d->openGLImageTextures.data(), SIGNAL(sigShowFloatingMessage(QString, int, bool)), SLOT(slotShowFloatingMessage(QString, int, bool))); setAcceptDrops(true); setAutoFillBackground(false); setFocusPolicy(Qt::StrongFocus); setAttribute(Qt::WA_NoSystemBackground, true); #ifdef Q_OS_MACOS setAttribute(Qt::WA_AcceptTouchEvents, false); #else setAttribute(Qt::WA_AcceptTouchEvents, true); #endif setAttribute(Qt::WA_InputMethodEnabled, false); setAttribute(Qt::WA_DontCreateNativeAncestors, true); #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) // we should make sure the texture doesn't have alpha channel, // otherwise blending will not work correctly. if (KisOpenGLModeProber::instance()->useHDRMode()) { setTextureFormat(GL_RGBA16F); } else { /** * When in pure OpenGL mode, the canvas surface will have alpha * channel. Therefore, if our canvas blending algorithm produces * semi-transparent pixels (and it does), then Krita window itself * will become transparent. Which is not good. * * In Angle mode, GL_RGB8 is not available (and the transparence effect * doesn't exist at all). */ if (!KisOpenGL::hasOpenGLES()) { setTextureFormat(GL_RGB8); } } #endif setDisplayFilterImpl(colorConverter->displayFilter(), true); connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); connect(KisConfigNotifier::instance(), SIGNAL(pixelGridModeChanged()), SLOT(slotPixelGridModeChanged())); slotConfigChanged(); slotPixelGridModeChanged(); cfg.writeEntry("canvasState", "OPENGL_SUCCESS"); } KisOpenGLCanvas2::~KisOpenGLCanvas2() { delete d; } void KisOpenGLCanvas2::setDisplayFilter(QSharedPointer displayFilter) { setDisplayFilterImpl(displayFilter, false); } void KisOpenGLCanvas2::setDisplayFilterImpl(QSharedPointer displayFilter, bool initializing) { bool needsInternalColorManagement = !displayFilter || displayFilter->useInternalColorManagement(); bool needsFullRefresh = d->openGLImageTextures->setInternalColorManagementActive(needsInternalColorManagement); d->displayFilter = displayFilter; if (!initializing && needsFullRefresh) { canvas()->startUpdateInPatches(canvas()->image()->bounds()); } else if (!initializing) { canvas()->updateCanvas(); } } void KisOpenGLCanvas2::notifyImageColorSpaceChanged(const KoColorSpace *cs) { // FIXME: on color space change the data is refetched multiple // times by different actors! if (d->openGLImageTextures->setImageColorSpace(cs)) { canvas()->startUpdateInPatches(canvas()->image()->bounds()); } } void KisOpenGLCanvas2::setWrapAroundViewingMode(bool value) { d->wrapAroundMode = value; update(); } inline void rectToVertices(QVector3D* vertices, const QRectF &rc) { vertices[0] = QVector3D(rc.left(), rc.bottom(), 0.f); vertices[1] = QVector3D(rc.left(), rc.top(), 0.f); vertices[2] = QVector3D(rc.right(), rc.bottom(), 0.f); vertices[3] = QVector3D(rc.left(), rc.top(), 0.f); vertices[4] = QVector3D(rc.right(), rc.top(), 0.f); vertices[5] = QVector3D(rc.right(), rc.bottom(), 0.f); } inline void rectToTexCoords(QVector2D* texCoords, const QRectF &rc) { texCoords[0] = QVector2D(rc.left(), rc.bottom()); texCoords[1] = QVector2D(rc.left(), rc.top()); texCoords[2] = QVector2D(rc.right(), rc.bottom()); texCoords[3] = QVector2D(rc.left(), rc.top()); texCoords[4] = QVector2D(rc.right(), rc.top()); texCoords[5] = QVector2D(rc.right(), rc.bottom()); } void KisOpenGLCanvas2::initializeGL() { KisOpenGL::initializeContext(context()); initializeOpenGLFunctions(); #if !defined(Q_OS_MACOS) && !defined(HAS_ONLY_OPENGL_ES) if (!KisOpenGL::hasOpenGLES()) { d->glFn201 = context()->versionFunctions(); if (!d->glFn201) { warnUI << "Cannot obtain QOpenGLFunctions_2_1, glLogicOp cannot be used"; } } else { d->glFn201 = nullptr; } #endif KisConfig cfg(true); d->openGLImageTextures->setProofingConfig(canvas()->proofingConfiguration()); d->openGLImageTextures->initGL(context()->functions()); d->openGLImageTextures->generateCheckerTexture(createCheckersImage(cfg.checkSize())); initializeShaders(); // If we support OpenGL 3.2, then prepare our VAOs and VBOs for drawing if (KisOpenGL::hasOpenGL3()) { d->quadVAO.create(); d->quadVAO.bind(); glEnableVertexAttribArray(PROGRAM_VERTEX_ATTRIBUTE); glEnableVertexAttribArray(PROGRAM_TEXCOORD_ATTRIBUTE); // Create the vertex buffer object, it has 6 vertices with 3 components d->quadBuffers[0].create(); d->quadBuffers[0].setUsagePattern(QOpenGLBuffer::StaticDraw); d->quadBuffers[0].bind(); d->quadBuffers[0].allocate(d->vertices, 6 * 3 * sizeof(float)); glVertexAttribPointer(PROGRAM_VERTEX_ATTRIBUTE, 3, GL_FLOAT, GL_FALSE, 0, 0); // Create the texture buffer object, it has 6 texture coordinates with 2 components d->quadBuffers[1].create(); d->quadBuffers[1].setUsagePattern(QOpenGLBuffer::StaticDraw); d->quadBuffers[1].bind(); d->quadBuffers[1].allocate(d->texCoords, 6 * 2 * sizeof(float)); glVertexAttribPointer(PROGRAM_TEXCOORD_ATTRIBUTE, 2, GL_FLOAT, GL_FALSE, 0, 0); // Create the outline buffer, this buffer will store the outlines of // tools and will frequently change data d->outlineVAO.create(); d->outlineVAO.bind(); glEnableVertexAttribArray(PROGRAM_VERTEX_ATTRIBUTE); // The outline buffer has a StreamDraw usage pattern, because it changes constantly d->lineBuffer.create(); d->lineBuffer.setUsagePattern(QOpenGLBuffer::StreamDraw); d->lineBuffer.bind(); glVertexAttribPointer(PROGRAM_VERTEX_ATTRIBUTE, 3, GL_FLOAT, GL_FALSE, 0, 0); } Sync::init(context()); d->canvasInitialized = true; } /** * Loads all shaders and reports compilation problems */ void KisOpenGLCanvas2::initializeShaders() { KIS_SAFE_ASSERT_RECOVER_RETURN(!d->canvasInitialized); delete d->checkerShader; delete d->solidColorShader; d->checkerShader = 0; d->solidColorShader = 0; try { d->checkerShader = d->shaderLoader.loadCheckerShader(); d->solidColorShader = d->shaderLoader.loadSolidColorShader(); } catch (const ShaderLoaderException &e) { reportFailedShaderCompilation(e.what()); } initializeDisplayShader(); } void KisOpenGLCanvas2::initializeDisplayShader() { KIS_SAFE_ASSERT_RECOVER_RETURN(!d->canvasInitialized); bool useHiQualityFiltering = d->filterMode == KisOpenGL::HighQualityFiltering; delete d->displayShader; d->displayShader = 0; try { d->displayShader = d->shaderLoader.loadDisplayShader(d->displayFilter, useHiQualityFiltering); d->displayShaderCompiledWithDisplayFilterSupport = d->displayFilter; } catch (const ShaderLoaderException &e) { reportFailedShaderCompilation(e.what()); } } /** * Displays a message box telling the user that * shader compilation failed and turns off OpenGL. */ void KisOpenGLCanvas2::reportFailedShaderCompilation(const QString &context) { KisConfig cfg(false); qDebug() << "Shader Compilation Failure: " << context; QMessageBox::critical(this, i18nc("@title:window", "Krita"), i18n("Krita could not initialize the OpenGL canvas:\n\n%1\n\n Krita will disable OpenGL and close now.", context), QMessageBox::Close); cfg.disableOpenGL(); cfg.setCanvasState("OPENGL_FAILED"); } void KisOpenGLCanvas2::resizeGL(int /*width*/, int /*height*/) { // The given size is the widget size but here we actually want to give // KisCoordinatesConverter the viewport size aligned to device pixels. coordinatesConverter()->setCanvasWidgetSize(widgetSizeAlignedToDevicePixel()); paintGL(); } void KisOpenGLCanvas2::paintGL() { if (!OPENGL_SUCCESS) { KisConfig cfg(false); cfg.writeEntry("canvasState", "OPENGL_PAINT_STARTED"); } KisOpenglCanvasDebugger::instance()->nofityPaintRequested(); renderCanvasGL(); if (d->glSyncObject) { Sync::deleteSync(d->glSyncObject); } d->glSyncObject = Sync::getSync(); QPainter gc(this); renderDecorations(&gc); gc.end(); if (!OPENGL_SUCCESS) { KisConfig cfg(false); cfg.writeEntry("canvasState", "OPENGL_SUCCESS"); OPENGL_SUCCESS = true; } } void KisOpenGLCanvas2::paintToolOutline(const QPainterPath &path) { if (!d->solidColorShader->bind()) { return; } QSizeF widgetSize = widgetSizeAlignedToDevicePixel(); // setup the mvp transformation QMatrix4x4 projectionMatrix; projectionMatrix.setToIdentity(); // FIXME: It may be better to have the projection in device pixel, but // this requires introducing a new coordinate system. projectionMatrix.ortho(0, widgetSize.width(), widgetSize.height(), 0, NEAR_VAL, FAR_VAL); // Set view/projection matrices QMatrix4x4 modelMatrix(coordinatesConverter()->flakeToWidgetTransform()); modelMatrix.optimize(); modelMatrix = projectionMatrix * modelMatrix; d->solidColorShader->setUniformValue(d->solidColorShader->location(Uniform::ModelViewProjection), modelMatrix); if (!KisOpenGL::hasOpenGLES()) { #ifndef HAS_ONLY_OPENGL_ES glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); glEnable(GL_COLOR_LOGIC_OP); #ifndef Q_OS_MACOS if (d->glFn201) { d->glFn201->glLogicOp(GL_XOR); } #else glLogicOp(GL_XOR); #endif // Q_OS_OSX #else // HAS_ONLY_OPENGL_ES KIS_ASSERT_X(false, "KisOpenGLCanvas2::paintToolOutline", "Unexpected KisOpenGL::hasOpenGLES returned false"); #endif // HAS_ONLY_OPENGL_ES } else { glEnable(GL_BLEND); glBlendFuncSeparate(GL_ONE_MINUS_DST_COLOR, GL_ZERO, GL_ONE, GL_ONE); } d->solidColorShader->setUniformValue( d->solidColorShader->location(Uniform::FragmentColor), QVector4D(d->cursorColor.redF(), d->cursorColor.greenF(), d->cursorColor.blueF(), 1.0f)); // Paint the tool outline if (KisOpenGL::hasOpenGL3()) { d->outlineVAO.bind(); d->lineBuffer.bind(); } // Convert every disjointed subpath to a polygon and draw that polygon QList subPathPolygons = path.toSubpathPolygons(); for (int i = 0; i < subPathPolygons.size(); i++) { const QPolygonF& polygon = subPathPolygons.at(i); QVector vertices; vertices.resize(polygon.count()); for (int j = 0; j < polygon.count(); j++) { QPointF p = polygon.at(j); vertices[j].setX(p.x()); vertices[j].setY(p.y()); } if (KisOpenGL::hasOpenGL3()) { d->lineBuffer.allocate(vertices.constData(), 3 * vertices.size() * sizeof(float)); } else { d->solidColorShader->enableAttributeArray(PROGRAM_VERTEX_ATTRIBUTE); d->solidColorShader->setAttributeArray(PROGRAM_VERTEX_ATTRIBUTE, vertices.constData()); } glDrawArrays(GL_LINE_STRIP, 0, vertices.size()); } if (KisOpenGL::hasOpenGL3()) { d->lineBuffer.release(); d->outlineVAO.release(); } if (!KisOpenGL::hasOpenGLES()) { #ifndef HAS_ONLY_OPENGL_ES glDisable(GL_COLOR_LOGIC_OP); #else KIS_ASSERT_X(false, "KisOpenGLCanvas2::paintToolOutline", "Unexpected KisOpenGL::hasOpenGLES returned false"); #endif } else { glDisable(GL_BLEND); } d->solidColorShader->release(); } bool KisOpenGLCanvas2::isBusy() const { const bool isBusyStatus = Sync::syncStatus(d->glSyncObject) == Sync::Unsignaled; KisOpenglCanvasDebugger::instance()->nofitySyncStatus(isBusyStatus); return isBusyStatus; } void KisOpenGLCanvas2::setLodResetInProgress(bool value) { d->lodSwitchInProgress = value; } void KisOpenGLCanvas2::drawCheckers() { if (!d->checkerShader) { return; } KisCoordinatesConverter *converter = coordinatesConverter(); QTransform textureTransform; QTransform modelTransform; QRectF textureRect; QRectF modelRect; QSizeF widgetSize = widgetSizeAlignedToDevicePixel(); QRectF viewportRect = !d->wrapAroundMode ? converter->imageRectInViewportPixels() : converter->widgetToViewport(QRectF(0, 0, widgetSize.width(), widgetSize.height())); if (!canvas()->renderingLimit().isEmpty()) { const QRect vrect = converter->imageToViewport(canvas()->renderingLimit()).toAlignedRect(); viewportRect &= vrect; } converter->getOpenGLCheckersInfo(viewportRect, &textureTransform, &modelTransform, &textureRect, &modelRect, d->scrollCheckers); textureTransform *= QTransform::fromScale(d->checkSizeScale / KisOpenGLImageTextures::BACKGROUND_TEXTURE_SIZE, d->checkSizeScale / KisOpenGLImageTextures::BACKGROUND_TEXTURE_SIZE); if (!d->checkerShader->bind()) { qWarning() << "Could not bind checker shader"; return; } QMatrix4x4 projectionMatrix; projectionMatrix.setToIdentity(); // FIXME: It may be better to have the projection in device pixel, but // this requires introducing a new coordinate system. projectionMatrix.ortho(0, widgetSize.width(), widgetSize.height(), 0, NEAR_VAL, FAR_VAL); // Set view/projection matrices QMatrix4x4 modelMatrix(modelTransform); modelMatrix.optimize(); modelMatrix = projectionMatrix * modelMatrix; d->checkerShader->setUniformValue(d->checkerShader->location(Uniform::ModelViewProjection), modelMatrix); QMatrix4x4 textureMatrix(textureTransform); d->checkerShader->setUniformValue(d->checkerShader->location(Uniform::TextureMatrix), textureMatrix); //Setup the geometry for rendering if (KisOpenGL::hasOpenGL3()) { rectToVertices(d->vertices, modelRect); d->quadBuffers[0].bind(); d->quadBuffers[0].write(0, d->vertices, 3 * 6 * sizeof(float)); rectToTexCoords(d->texCoords, textureRect); d->quadBuffers[1].bind(); d->quadBuffers[1].write(0, d->texCoords, 2 * 6 * sizeof(float)); } else { rectToVertices(d->vertices, modelRect); d->checkerShader->enableAttributeArray(PROGRAM_VERTEX_ATTRIBUTE); d->checkerShader->setAttributeArray(PROGRAM_VERTEX_ATTRIBUTE, d->vertices); rectToTexCoords(d->texCoords, textureRect); d->checkerShader->enableAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE); d->checkerShader->setAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE, d->texCoords); } // render checkers glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, d->openGLImageTextures->checkerTexture()); glDrawArrays(GL_TRIANGLES, 0, 6); glBindTexture(GL_TEXTURE_2D, 0); d->checkerShader->release(); glBindBuffer(GL_ARRAY_BUFFER, 0); } void KisOpenGLCanvas2::drawGrid() { if (!d->solidColorShader->bind()) { return; } QSizeF widgetSize = widgetSizeAlignedToDevicePixel(); QMatrix4x4 projectionMatrix; projectionMatrix.setToIdentity(); // FIXME: It may be better to have the projection in device pixel, but // this requires introducing a new coordinate system. projectionMatrix.ortho(0, widgetSize.width(), widgetSize.height(), 0, NEAR_VAL, FAR_VAL); // Set view/projection matrices QMatrix4x4 modelMatrix(coordinatesConverter()->imageToWidgetTransform()); modelMatrix.optimize(); modelMatrix = projectionMatrix * modelMatrix; d->solidColorShader->setUniformValue(d->solidColorShader->location(Uniform::ModelViewProjection), modelMatrix); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); d->solidColorShader->setUniformValue( d->solidColorShader->location(Uniform::FragmentColor), QVector4D(d->gridColor.redF(), d->gridColor.greenF(), d->gridColor.blueF(), 0.5f)); if (KisOpenGL::hasOpenGL3()) { d->outlineVAO.bind(); d->lineBuffer.bind(); } QRectF widgetRect(0,0, widgetSize.width(), widgetSize.height()); QRectF widgetRectInImagePixels = coordinatesConverter()->documentToImage(coordinatesConverter()->widgetToDocument(widgetRect)); QRect wr = widgetRectInImagePixels.toAlignedRect(); if (!d->wrapAroundMode) { wr &= d->openGLImageTextures->storedImageBounds(); } QPoint topLeftCorner = wr.topLeft(); QPoint bottomRightCorner = wr.bottomRight() + QPoint(1, 1); QVector grid; for (int i = topLeftCorner.x(); i <= bottomRightCorner.x(); ++i) { grid.append(QVector3D(i, topLeftCorner.y(), 0)); grid.append(QVector3D(i, bottomRightCorner.y(), 0)); } for (int i = topLeftCorner.y(); i <= bottomRightCorner.y(); ++i) { grid.append(QVector3D(topLeftCorner.x(), i, 0)); grid.append(QVector3D(bottomRightCorner.x(), i, 0)); } if (KisOpenGL::hasOpenGL3()) { d->lineBuffer.allocate(grid.constData(), 3 * grid.size() * sizeof(float)); } else { d->solidColorShader->enableAttributeArray(PROGRAM_VERTEX_ATTRIBUTE); d->solidColorShader->setAttributeArray(PROGRAM_VERTEX_ATTRIBUTE, grid.constData()); } glDrawArrays(GL_LINES, 0, grid.size()); if (KisOpenGL::hasOpenGL3()) { d->lineBuffer.release(); d->outlineVAO.release(); } d->solidColorShader->release(); glDisable(GL_BLEND); } void KisOpenGLCanvas2::drawImage() { if (!d->displayShader) { return; } glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); KisCoordinatesConverter *converter = coordinatesConverter(); d->displayShader->bind(); QSizeF widgetSize = widgetSizeAlignedToDevicePixel(); QMatrix4x4 projectionMatrix; projectionMatrix.setToIdentity(); // FIXME: It may be better to have the projection in device pixel, but // this requires introducing a new coordinate system. projectionMatrix.ortho(0, widgetSize.width(), widgetSize.height(), 0, NEAR_VAL, FAR_VAL); // Set view/projection matrices QMatrix4x4 modelMatrix(converter->imageToWidgetTransform()); modelMatrix.optimize(); modelMatrix = projectionMatrix * modelMatrix; d->displayShader->setUniformValue(d->displayShader->location(Uniform::ModelViewProjection), modelMatrix); QMatrix4x4 textureMatrix; textureMatrix.setToIdentity(); d->displayShader->setUniformValue(d->displayShader->location(Uniform::TextureMatrix), textureMatrix); QRectF widgetRect(0,0, widgetSize.width(), widgetSize.height()); QRectF widgetRectInImagePixels = converter->documentToImage(converter->widgetToDocument(widgetRect)); const QRect renderingLimit = canvas()->renderingLimit(); if (!renderingLimit.isEmpty()) { widgetRectInImagePixels &= renderingLimit; } qreal scaleX, scaleY; converter->imagePhysicalScale(&scaleX, &scaleY); d->displayShader->setUniformValue(d->displayShader->location(Uniform::ViewportScale), (GLfloat) scaleX); d->displayShader->setUniformValue(d->displayShader->location(Uniform::TexelSize), (GLfloat) d->openGLImageTextures->texelSize()); QRect ir = d->openGLImageTextures->storedImageBounds(); QRect wr = widgetRectInImagePixels.toAlignedRect(); if (!d->wrapAroundMode) { // if we don't want to paint wrapping images, just limit the // processing area, and the code will handle all the rest wr &= ir; } int firstColumn = d->xToColWithWrapCompensation(wr.left(), ir); int lastColumn = d->xToColWithWrapCompensation(wr.right(), ir); int firstRow = d->yToRowWithWrapCompensation(wr.top(), ir); int lastRow = d->yToRowWithWrapCompensation(wr.bottom(), ir); int minColumn = d->openGLImageTextures->xToCol(ir.left()); int maxColumn = d->openGLImageTextures->xToCol(ir.right()); int minRow = d->openGLImageTextures->yToRow(ir.top()); int maxRow = d->openGLImageTextures->yToRow(ir.bottom()); int imageColumns = maxColumn - minColumn + 1; int imageRows = maxRow - minRow + 1; for (int col = firstColumn; col <= lastColumn; col++) { for (int row = firstRow; row <= lastRow; row++) { int effectiveCol = col; int effectiveRow = row; QPointF tileWrappingTranslation; if (effectiveCol > maxColumn || effectiveCol < minColumn) { int translationStep = floor(qreal(col) / imageColumns); int originCol = translationStep * imageColumns; effectiveCol = col - originCol; tileWrappingTranslation.rx() = translationStep * ir.width(); } if (effectiveRow > maxRow || effectiveRow < minRow) { int translationStep = floor(qreal(row) / imageRows); int originRow = translationStep * imageRows; effectiveRow = row - originRow; tileWrappingTranslation.ry() = translationStep * ir.height(); } KisTextureTile *tile = d->openGLImageTextures->getTextureTileCR(effectiveCol, effectiveRow); if (!tile) { warnUI << "OpenGL: Trying to paint texture tile but it has not been created yet."; continue; } /* * We create a float rect here to workaround Qt's * "history reasons" in calculation of right() * and bottom() coordinates of integer rects. */ QRectF textureRect; QRectF modelRect; if (renderingLimit.isEmpty()) { textureRect = tile->tileRectInTexturePixels(); modelRect = tile->tileRectInImagePixels().translated(tileWrappingTranslation.x(), tileWrappingTranslation.y()); } else { const QRect limitedTileRect = tile->tileRectInImagePixels() & renderingLimit; textureRect = tile->imageRectInTexturePixels(limitedTileRect); modelRect = limitedTileRect.translated(tileWrappingTranslation.x(), tileWrappingTranslation.y()); } //Setup the geometry for rendering if (KisOpenGL::hasOpenGL3()) { rectToVertices(d->vertices, modelRect); d->quadBuffers[0].bind(); d->quadBuffers[0].write(0, d->vertices, 3 * 6 * sizeof(float)); rectToTexCoords(d->texCoords, textureRect); d->quadBuffers[1].bind(); d->quadBuffers[1].write(0, d->texCoords, 2 * 6 * sizeof(float)); } else { rectToVertices(d->vertices, modelRect); d->displayShader->enableAttributeArray(PROGRAM_VERTEX_ATTRIBUTE); d->displayShader->setAttributeArray(PROGRAM_VERTEX_ATTRIBUTE, d->vertices); rectToTexCoords(d->texCoords, textureRect); d->displayShader->enableAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE); d->displayShader->setAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE, d->texCoords); } if (d->displayFilter) { glActiveTexture(GL_TEXTURE0 + 1); glBindTexture(GL_TEXTURE_3D, d->displayFilter->lutTexture()); d->displayShader->setUniformValue(d->displayShader->location(Uniform::Texture1), 1); } glActiveTexture(GL_TEXTURE0); const int currentLodPlane = tile->bindToActiveTexture(d->lodSwitchInProgress); if (d->displayShader->location(Uniform::FixedLodLevel) >= 0) { d->displayShader->setUniformValue(d->displayShader->location(Uniform::FixedLodLevel), (GLfloat) currentLodPlane); } if (currentLodPlane > 0) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST); } else if (SCALE_MORE_OR_EQUAL_TO(scaleX, scaleY, 2.0)) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); } else { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); switch(d->filterMode) { case KisOpenGL::NearestFilterMode: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); break; case KisOpenGL::BilinearFilterMode: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); break; case KisOpenGL::TrilinearFilterMode: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); break; case KisOpenGL::HighQualityFiltering: if (SCALE_LESS_THAN(scaleX, scaleY, 0.5)) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST); } else { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); } break; } } glDrawArrays(GL_TRIANGLES, 0, 6); } } glBindTexture(GL_TEXTURE_2D, 0); d->displayShader->release(); glBindBuffer(GL_ARRAY_BUFFER, 0); glDisable(GL_BLEND); } QSize KisOpenGLCanvas2::viewportDevicePixelSize() const { // This is how QOpenGLCanvas sets the FBO and the viewport size. If // devicePixelRatioF() is non-integral, the result is truncated. int viewportWidth = static_cast(width() * devicePixelRatioF()); int viewportHeight = static_cast(height() * devicePixelRatioF()); return QSize(viewportWidth, viewportHeight); } QSizeF KisOpenGLCanvas2::widgetSizeAlignedToDevicePixel() const { QSize viewportSize = viewportDevicePixelSize(); qreal scaledWidth = viewportSize.width() / devicePixelRatioF(); qreal scaledHeight = viewportSize.height() / devicePixelRatioF(); return QSizeF(scaledWidth, scaledHeight); } void KisOpenGLCanvas2::slotConfigChanged() { KisConfig cfg(true); d->checkSizeScale = KisOpenGLImageTextures::BACKGROUND_TEXTURE_CHECK_SIZE / static_cast(cfg.checkSize()); d->scrollCheckers = cfg.scrollCheckers(); d->openGLImageTextures->generateCheckerTexture(createCheckersImage(cfg.checkSize())); d->openGLImageTextures->updateConfig(cfg.useOpenGLTextureBuffer(), cfg.numMipmapLevels()); d->filterMode = (KisOpenGL::FilterMode) cfg.openGLFilteringMode(); d->cursorColor = cfg.getCursorMainColor(); notifyConfigChanged(); } void KisOpenGLCanvas2::slotPixelGridModeChanged() { KisConfig cfg(true); d->pixelGridDrawingThreshold = cfg.getPixelGridDrawingThreshold(); d->pixelGridEnabled = cfg.pixelGridEnabled(); d->gridColor = cfg.getPixelGridColor(); update(); } void KisOpenGLCanvas2::slotShowFloatingMessage(const QString &message, int timeout, bool priority) { canvas()->imageView()->showFloatingMessage(message, QIcon(), timeout, priority ? KisFloatingMessage::High : KisFloatingMessage::Medium); } QVariant KisOpenGLCanvas2::inputMethodQuery(Qt::InputMethodQuery query) const { return processInputMethodQuery(query); } void KisOpenGLCanvas2::inputMethodEvent(QInputMethodEvent *event) { processInputMethodEvent(event); } void KisOpenGLCanvas2::renderCanvasGL() { { // Draw the border (that is, clear the whole widget to the border color) QColor widgetBackgroundColor = borderColor(); const KoColorSpace *finalColorSpace = KoColorSpaceRegistry::instance()->colorSpace(RGBAColorModelID.id(), d->openGLImageTextures->updateInfoBuilder().destinationColorSpace()->colorDepthId().id(), d->openGLImageTextures->monitorProfile()); KoColor convertedBackgroudColor = KoColor(widgetBackgroundColor, KoColorSpaceRegistry::instance()->rgb8()); convertedBackgroudColor.convertTo(finalColorSpace); QVector channels = QVector(4); convertedBackgroudColor.colorSpace()->normalisedChannelsValue(convertedBackgroudColor.data(), channels); glClearColor(channels[0], channels[1], channels[2], 1.0); } glClear(GL_COLOR_BUFFER_BIT); if ((d->displayFilter && d->displayFilter->updateShader()) || (bool(d->displayFilter) != d->displayShaderCompiledWithDisplayFilterSupport)) { KIS_SAFE_ASSERT_RECOVER_NOOP(d->canvasInitialized); d->canvasInitialized = false; // TODO: check if actually needed? initializeDisplayShader(); d->canvasInitialized = true; } if (KisOpenGL::hasOpenGL3()) { d->quadVAO.bind(); } drawCheckers(); drawImage(); if ((coordinatesConverter()->effectiveZoom() > d->pixelGridDrawingThreshold - 0.00001) && d->pixelGridEnabled) { drawGrid(); } if (KisOpenGL::hasOpenGL3()) { d->quadVAO.release(); } } void KisOpenGLCanvas2::renderDecorations(QPainter *painter) { QRect boundingRect = coordinatesConverter()->imageRectInWidgetPixels().toAlignedRect(); drawDecorations(*painter, boundingRect); } void KisOpenGLCanvas2::setDisplayColorConverter(KisDisplayColorConverter *colorConverter) { d->openGLImageTextures->setMonitorProfile(colorConverter->openGLCanvasSurfaceProfile(), colorConverter->renderingIntent(), colorConverter->conversionFlags()); } void KisOpenGLCanvas2::channelSelectionChanged(const QBitArray &channelFlags) { d->openGLImageTextures->setChannelFlags(channelFlags); } void KisOpenGLCanvas2::finishResizingImage(qint32 w, qint32 h) { if (d->canvasInitialized) { d->openGLImageTextures->slotImageSizeChanged(w, h); } } KisUpdateInfoSP KisOpenGLCanvas2::startUpdateCanvasProjection(const QRect & rc, const QBitArray &channelFlags) { d->openGLImageTextures->setChannelFlags(channelFlags); if (canvas()->proofingConfigUpdated()) { d->openGLImageTextures->setProofingConfig(canvas()->proofingConfiguration()); canvas()->setProofingConfigUpdated(false); } return d->openGLImageTextures->updateCache(rc, d->openGLImageTextures->image()); } QRect KisOpenGLCanvas2::updateCanvasProjection(KisUpdateInfoSP info) { // See KisQPainterCanvas::updateCanvasProjection for more info bool isOpenGLUpdateInfo = dynamic_cast(info.data()); if (isOpenGLUpdateInfo) { d->openGLImageTextures->recalculateCache(info, d->lodSwitchInProgress); } return QRect(); // FIXME: Implement dirty rect for OpenGL } QVector KisOpenGLCanvas2::updateCanvasProjection(const QVector &infoObjects) { #ifdef Q_OS_MACOS /** - * On OSX openGL defferent (shared) contexts have different execution queues. + * On OSX openGL different (shared) contexts have different execution queues. * It means that the textures uploading and their painting can be easily reordered. * To overcome the issue, we should ensure that the textures are uploaded in the * same openGL context as the painting is done. */ QOpenGLContext *oldContext = QOpenGLContext::currentContext(); QSurface *oldSurface = oldContext ? oldContext->surface() : 0; this->makeCurrent(); #endif QVector result = KisCanvasWidgetBase::updateCanvasProjection(infoObjects); #ifdef Q_OS_MACOS if (oldContext) { oldContext->makeCurrent(oldSurface); } else { this->doneCurrent(); } #endif return result; } bool KisOpenGLCanvas2::callFocusNextPrevChild(bool next) { return focusNextPrevChild(next); } KisOpenGLImageTexturesSP KisOpenGLCanvas2::openGLImageTextures() const { return d->openGLImageTextures; } diff --git a/libs/ui/tool/kis_tool.h b/libs/ui/tool/kis_tool.h index fdbd5e67f9..00d15c6796 100644 --- a/libs/ui/tool/kis_tool.h +++ b/libs/ui/tool/kis_tool.h @@ -1,329 +1,329 @@ /* * 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_TOOL_H_ #define KIS_TOOL_H_ #include #include #include #include #include #include #include #ifdef __GNUC__ #define WARN_WRONG_MODE(_mode) warnKrita << "Unexpected tool event has come to" << __func__ << "while being mode" << _mode << "!" #else #define WARN_WRONG_MODE(_mode) warnKrita << "Unexpected tool event has come while being mode" << _mode << "!" #endif #define CHECK_MODE_SANITY_OR_RETURN(_mode) if (mode() != _mode) { WARN_WRONG_MODE(mode()); return; } class KoCanvasBase; class KoPattern; class KoAbstractGradient; class KisFilterConfiguration; class QPainter; class QPainterPath; class QPolygonF; /// Definitions of the toolgroups of Krita static const QString TOOL_TYPE_SHAPE = "0 Krita/Shape"; // Geometric shapes like ellipses and lines static const QString TOOL_TYPE_TRANSFORM = "2 Krita/Transform"; // Tools that transform the layer; static const QString TOOL_TYPE_FILL = "3 Krita/Fill"; // Tools that fill parts of the canvas static const QString TOOL_TYPE_VIEW = "4 Krita/View"; // Tools that affect the canvas: pan, zoom, etc. static const QString TOOL_TYPE_SELECTION = "5 Krita/Select"; // Tools that select pixels //activation id for Krita tools, Krita tools are always active and handle locked and invisible layers by themself static const QString KRITA_TOOL_ACTIVATION_ID = "flake/always"; class KRITAUI_EXPORT KisTool : public KoToolBase { Q_OBJECT Q_PROPERTY(bool isActive READ isActive NOTIFY isActiveChanged) public: enum { FLAG_USES_CUSTOM_PRESET=0x01, FLAG_USES_CUSTOM_COMPOSITEOP=0x02, FLAG_USES_CUSTOM_SIZE=0x04 }; KisTool(KoCanvasBase * canvas, const QCursor & cursor); ~KisTool() override; virtual int flags() const { return 0; } void deleteSelection() override; // KoToolBase Implementation. public: /** * Called by KisToolProxy when the primary action of the tool is * going to be started now, that is when all the modifiers are * pressed and the only thing left is just to press the mouse * button. On coming of this callback the tool is supposed to * prepare the cursor and/or the outline to show the user shat is * going to happen next */ virtual void activatePrimaryAction(); /** * Called by KisToolProxy when the primary is no longer possible * to be started now, e.g. when its modifiers and released. The - * tool is supposed revert all the preparetions it has doen in + * tool is supposed to revert all the preparations it has done in * activatePrimaryAction(). */ virtual void deactivatePrimaryAction(); /** * Called by KisToolProxy when a primary action for the tool is * started. The \p event stores the original event that * started the stroke. The \p event is _accepted_ by default. If * the tool decides to ignore this particular action (e.g. when * the node is not editable), it should call event->ignore(). Then * no further continuePrimaryAction() or endPrimaryAction() will * be called until the next user action. */ virtual void beginPrimaryAction(KoPointerEvent *event); /** * Called by KisToolProxy when the primary action is in progress * of pointer movement. If the tool has ignored the event in * beginPrimaryAction(), this method will not be called. */ virtual void continuePrimaryAction(KoPointerEvent *event); /** * Called by KisToolProxy when the primary action is being * finished, that is while mouseRelease or tabletRelease event. * If the tool has ignored the event in beginPrimaryAction(), this * method will not be called. */ virtual void endPrimaryAction(KoPointerEvent *event); /** * The same as beginPrimaryAction(), but called when the stroke is * started by a double-click * * \see beginPrimaryAction() */ virtual void beginPrimaryDoubleClickAction(KoPointerEvent *event); /** * Returns true if the tool can handle (and wants to handle) a * very tight flow of input events from the tablet */ virtual bool primaryActionSupportsHiResEvents() const; enum ToolAction { Primary, AlternateChangeSize, AlternatePickFgNode, AlternatePickBgNode, AlternatePickFgImage, AlternatePickBgImage, AlternateSecondary, AlternateThird, AlternateFourth, AlternateFifth, Alternate_NONE = 10000 }; // Technically users are allowed to configure this, but nobody ever would do that. // So these can basically be thought of as aliases to ctrl+click, etc. enum AlternateAction { ChangeSize = AlternateChangeSize, // Default: Shift+Left click PickFgNode = AlternatePickFgNode, // Default: Ctrl+Alt+Left click PickBgNode = AlternatePickBgNode, // Default: Ctrl+Alt+Right click PickFgImage = AlternatePickFgImage, // Default: Ctrl+Left click PickBgImage = AlternatePickBgImage, // Default: Ctrl+Right click Secondary = AlternateSecondary, Third = AlternateThird, Fourth = AlternateFourth, Fifth = AlternateFifth, NONE = 10000 }; enum NodePaintAbility { VECTOR, CLONE, PAINT, UNPAINTABLE }; Q_ENUMS(NodePaintAbility) static AlternateAction actionToAlternateAction(ToolAction action); virtual void activateAlternateAction(AlternateAction action); virtual void deactivateAlternateAction(AlternateAction action); virtual void beginAlternateAction(KoPointerEvent *event, AlternateAction action); virtual void continueAlternateAction(KoPointerEvent *event, AlternateAction action); virtual void endAlternateAction(KoPointerEvent *event, AlternateAction action); virtual void beginAlternateDoubleClickAction(KoPointerEvent *event, AlternateAction action); void mousePressEvent(KoPointerEvent *event) override; void mouseDoubleClickEvent(KoPointerEvent *event) override; void mouseTripleClickEvent(KoPointerEvent *event) override; void mouseReleaseEvent(KoPointerEvent *event) override; void mouseMoveEvent(KoPointerEvent *event) override; bool isActive() const; KisTool::NodePaintAbility nodePaintAbility(); public Q_SLOTS: void activate(ToolActivation activation, const QSet &shapes) override; void deactivate() override; void canvasResourceChanged(int key, const QVariant & res) override; // Implement this slot in case there are any widgets or properties which need // to be updated after certain operations, to reflect the inner state correctly. // At the moment this is used for smoothing options in the freehand brush, but // this will likely be expanded. virtual void updateSettingsViews(); Q_SIGNALS: void isActiveChanged(bool isActivated); protected: // conversion methods are also needed by the paint information builder friend class KisToolPaintingInformationBuilder; /// Convert from native (postscript points) to image pixel /// coordinates. QPointF convertToPixelCoord(KoPointerEvent *e); QPointF convertToPixelCoord(const QPointF& pt); QPointF convertToPixelCoordAndSnap(KoPointerEvent *e, const QPointF &offset = QPointF(), bool useModifiers = true); QPointF convertToPixelCoordAndSnap(const QPointF& pt, const QPointF &offset = QPointF()); protected: QPointF widgetCenterInWidgetPixels(); QPointF convertDocumentToWidget(const QPointF& pt); /// Convert from native (postscript points) to integer image pixel /// coordinates. This rounds down (not truncate) the pixel coordinates and /// should be used in preference to QPointF::toPoint(), which rounds, /// to ensure the cursor acts on the pixel it is visually over. QPoint convertToImagePixelCoordFloored(KoPointerEvent *e); QRectF convertToPt(const QRectF &rect); qreal convertToPt(qreal value); QPointF viewToPixel(const QPointF &viewCoord) const; /// Convert an integer pixel coordinate into a view coordinate. /// The view coordinate is at the centre of the pixel. QPointF pixelToView(const QPoint &pixelCoord) const; /// Convert a floating point pixel coordinate into a view coordinate. QPointF pixelToView(const QPointF &pixelCoord) const; /// Convert a pixel rectangle into a view rectangle. QRectF pixelToView(const QRectF &pixelRect) const; /// Convert a pixel path into a view path QPainterPath pixelToView(const QPainterPath &pixelPath) const; /// Convert a pixel polygon into a view path QPolygonF pixelToView(const QPolygonF &pixelPolygon) const; /// Update the canvas for the given rectangle in image pixel coordinates. void updateCanvasPixelRect(const QRectF &pixelRect); /// Update the canvas for the given rectangle in view coordinates. void updateCanvasViewRect(const QRectF &viewRect); QWidget* createOptionWidget() override; /** * To determine whether this tool will change its behavior when * modifier keys are pressed */ virtual bool listeningToModifiers(); /** * Request that this tool no longer listen to modifier keys * (Responding to the request is optional) */ virtual void listenToModifiers(bool listen); protected: KisImageWSP image() const; QCursor cursor() const; /// Call this to set the document modified void notifyModified() const; KisImageWSP currentImage(); KoPattern* currentPattern(); KoAbstractGradient *currentGradient(); KisNodeSP currentNode() const; KisNodeList selectedNodes() const; KoColor currentFgColor(); KoColor currentBgColor(); KisPaintOpPresetSP currentPaintOpPreset(); KisFilterConfigurationSP currentGenerator(); /// paint the path which is in view coordinates, default paint mode is XOR_MODE, BW_MODE is also possible /// never apply transformations to the painter, they would be useless, if drawing in OpenGL mode. The coordinates in the path should be in view coordinates. void paintToolOutline(QPainter * painter, const QPainterPath &path); /// Checks checks if the current node is editable bool nodeEditable(); /// Checks checks if the selection is editable, only applies to local selection as global selection is always editable bool selectionEditable(); /// Override the cursor appropriately if current node is not editable bool overrideCursorIfNotEditable(); bool blockUntilOperationsFinished(); void blockUntilOperationsFinishedForced(); protected: enum ToolMode: int { HOVER_MODE, PAINT_MODE, SECONDARY_PAINT_MODE, MIRROR_AXIS_SETUP_MODE, GESTURE_MODE, PAN_MODE, OTHER, // tool-specific modes, like multibrush's symmetry axis setup OTHER_1 }; virtual void setMode(ToolMode mode); virtual ToolMode mode() const; void setCursor(const QCursor &cursor); protected Q_SLOTS: /** * Called whenever the configuration settings change. */ virtual void resetCursorStyle(); private: struct Private; Private* const d; }; #endif // KIS_TOOL_H_ diff --git a/libs/ui/widgets/KisNewsWidget.cpp b/libs/ui/widgets/KisNewsWidget.cpp index 273a05f973..73c5bbf048 100644 --- a/libs/ui/widgets/KisNewsWidget.cpp +++ b/libs/ui/widgets/KisNewsWidget.cpp @@ -1,223 +1,223 @@ /* * Copyright (c) 2018 boud * * 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 "KisNewsWidget.h" #include #include #include #include #include #include #include #include "kis_config.h" #include "KisMultiFeedRSSModel.h" #include "QRegularExpression" KisNewsDelegate::KisNewsDelegate(QObject *parent) : QStyledItemDelegate(parent) { } void KisNewsDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { painter->save(); QStyleOptionViewItem optionCopy = option; initStyleOption(&optionCopy, index); QStyle *style = optionCopy.widget? optionCopy.widget->style() : QApplication::style(); QTextDocument doc; doc.setHtml(optionCopy.text); doc.setDocumentMargin(10); /// Painting item without text optionCopy.text = QString(); style->drawControl(QStyle::CE_ItemViewItem, &optionCopy, painter); QAbstractTextDocumentLayout::PaintContext ctx; // Highlighting text if item is selected if (optionCopy.state & QStyle::State_Selected) { ctx.palette.setColor(QPalette::Text, optionCopy.palette.color(QPalette::Active, QPalette::HighlightedText)); } painter->translate(optionCopy.rect.left(), optionCopy.rect.top()); QRect clip(0, 0, optionCopy.rect.width(), optionCopy.rect.height()); doc.setPageSize(clip.size()); doc.drawContents(painter, clip); painter->restore(); } QSize KisNewsDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { QStyleOptionViewItem optionCopy = option; initStyleOption(&optionCopy, index); QTextDocument doc; doc.setHtml(optionCopy.text); doc.setTextWidth(optionCopy.rect.width()); return QSize(doc.idealWidth(), doc.size().height()); } KisNewsWidget::KisNewsWidget(QWidget *parent) : QWidget(parent) { setupUi(this); m_rssModel = new MultiFeedRssModel(this); connect(m_rssModel, SIGNAL(feedDataChanged()), this, SLOT(rssDataChanged())); setCursor(Qt::PointingHandCursor); listNews->setModel(m_rssModel); listNews->setItemDelegate(new KisNewsDelegate(listNews)); connect(listNews, SIGNAL(clicked(QModelIndex)), this, SLOT(itemSelected(QModelIndex))); } void KisNewsWidget::setAnalyticsTracking(QString text) { analyticsTrackingParameters = text; } bool KisNewsWidget::hasUpdateAvailable() { return needsVersionUpdate; } QString KisNewsWidget::versionNumber() { return newVersionNumber; } QString KisNewsWidget::versionLink() { return newVersionLink; } void KisNewsWidget::toggleNews(bool toggle) { KisConfig cfg(false); cfg.writeEntry("FetchNews", toggle); if (toggle) { m_rssModel->addFeed(QLatin1String("https://krita.org/en/feed/")); } else { m_rssModel->removeFeed(QLatin1String("https://krita.org/en/feed/")); } } void KisNewsWidget::itemSelected(const QModelIndex &idx) { if (idx.isValid()) { QString link = idx.data(RssRoles::LinkRole).toString(); // append query string for analytics tracking if we set it if (analyticsTrackingParameters != "") { // use title in analytics query string QString linkTitle = idx.data(RssRoles::TitleRole).toString(); linkTitle = linkTitle.simplified(); // trims and makes 1 white space linkTitle = linkTitle.replace(" ", ""); analyticsTrackingParameters = analyticsTrackingParameters.append(linkTitle); QDesktopServices::openUrl(QUrl(link.append(analyticsTrackingParameters))); } else { QDesktopServices::openUrl(QUrl(link)); } } } void KisNewsWidget::rssDataChanged() { // grab the latest release post and URL for reference later // if we need to update for (int i = 0; i < m_rssModel->rowCount(); i++) { const QModelIndex &idx = m_rssModel->index(i); if (idx.isValid()) { // only use official release announcements to get version number if ( idx.data(RssRoles::CategoryRole).toString() != "Official Release") { continue; } QString linkTitle = idx.data(RssRoles::TitleRole).toString(); // regex to capture version number QRegularExpression versionRegex("\\d\\.\\d\\.?\\d?\\.?\\d"); QRegularExpressionMatch matched = versionRegex.match(linkTitle); // only take the top match for release version since that is the newest if (matched.hasMatch()) { newVersionNumber = matched.captured(0); newVersionLink = idx.data(RssRoles::LinkRole).toString(); break; } } } // see if we need to update our version, or we are on a dev version calculateVersionUpdateStatus(); emit newsDataChanged(); } void KisNewsWidget::calculateVersionUpdateStatus() { // do version compare to see if there is a new version available // also check to see if we are on a dev version (newer than newest release) QStringList currentVersionParts = qApp->applicationVersion().split("."); QStringList onlineReleaseAnnouncement = newVersionNumber.split("."); // is the major version different? if (onlineReleaseAnnouncement[0] > currentVersionParts[0] ) { needsVersionUpdate = true; // we are a major version behind return; } // major versions are the same, so check minor versions if (onlineReleaseAnnouncement[1] > currentVersionParts[1] ) { needsVersionUpdate = true; // we are a minor version behind return; } // minor versions are the same, so maybe bugfix version is different - // sometimes we don't communicate this, implictly make 0 if it doesn't exist + // sometimes we don't communicate this, implicitly make 0 if it doesn't exist if (onlineReleaseAnnouncement[2].isNull()) { onlineReleaseAnnouncement[2] = "0"; } if (currentVersionParts[2].isNull()) { currentVersionParts[2] = "0"; } if (onlineReleaseAnnouncement[2] > currentVersionParts[2] ) { needsVersionUpdate = true; // we are a bugfix version behind return; } } diff --git a/libs/ui/widgets/kis_advanced_color_space_selector.cc b/libs/ui/widgets/kis_advanced_color_space_selector.cc index c23a15bd56..2550e89840 100644 --- a/libs/ui/widgets/kis_advanced_color_space_selector.cc +++ b/libs/ui/widgets/kis_advanced_color_space_selector.cc @@ -1,795 +1,795 @@ /* * Copyright (C) 2007 Cyrille Berger * Copyright (C) 2011 Boudewijn Rempt * Copyright (C) 2011 Srikanth Tiyyagura * Copyright (C) 2015 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_advanced_color_space_selector.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ui_wdgcolorspaceselectoradvanced.h" #include struct KisAdvancedColorSpaceSelector::Private { Ui_WdgColorSpaceSelectorAdvanced* colorSpaceSelector; QString knsrcFile; }; KisAdvancedColorSpaceSelector::KisAdvancedColorSpaceSelector(QWidget* parent, const QString &caption) : QDialog(parent) , d(new Private) { setWindowTitle(caption); d->colorSpaceSelector = new Ui_WdgColorSpaceSelectorAdvanced; d->colorSpaceSelector->setupUi(this); d->colorSpaceSelector->cmbColorModels->setIDList(KoColorSpaceRegistry::instance()->colorModelsList(KoColorSpaceRegistry::OnlyUserVisible)); fillCmbDepths(d->colorSpaceSelector->cmbColorModels->currentItem()); d->colorSpaceSelector->bnInstallProfile->setIcon(KisIconUtils::loadIcon("document-open")); d->colorSpaceSelector->bnInstallProfile->setToolTip( i18n("Open Color Profile") ); connect(d->colorSpaceSelector->cmbColorModels, SIGNAL(activated(KoID)), this, SLOT(fillCmbDepths(KoID))); connect(d->colorSpaceSelector->cmbColorDepth, SIGNAL(activated(KoID)), this, SLOT(fillLstProfiles())); connect(d->colorSpaceSelector->cmbColorModels, SIGNAL(activated(KoID)), this, SLOT(fillLstProfiles())); connect(d->colorSpaceSelector->lstProfile, SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)), this, SLOT(colorSpaceChanged())); connect(this, SIGNAL(selectionChanged(bool)), this, SLOT(fillDescription())); connect(this, SIGNAL(selectionChanged(bool)), d->colorSpaceSelector->TongueWidget, SLOT(repaint())); connect(this, SIGNAL(selectionChanged(bool)), d->colorSpaceSelector->TRCwidget, SLOT(repaint())); connect(d->colorSpaceSelector->bnInstallProfile, SIGNAL(clicked()), this, SLOT(installProfile())); connect(d->colorSpaceSelector->bnOK, SIGNAL(accepted()), this, SLOT(accept())); connect(d->colorSpaceSelector->bnOK, SIGNAL(rejected()), this, SLOT(reject())); fillLstProfiles(); } KisAdvancedColorSpaceSelector::~KisAdvancedColorSpaceSelector() { delete d->colorSpaceSelector; delete d; } void KisAdvancedColorSpaceSelector::fillLstProfiles() { d->colorSpaceSelector->lstProfile->blockSignals(true); const QString colorSpaceId = KoColorSpaceRegistry::instance()->colorSpaceId(d->colorSpaceSelector->cmbColorModels->currentItem(), d->colorSpaceSelector->cmbColorDepth->currentItem()); const QString defaultProfileName = KoColorSpaceRegistry::instance()->defaultProfileForColorSpace(colorSpaceId); d->colorSpaceSelector->lstProfile->clear(); QList profileList = KoColorSpaceRegistry::instance()->profilesFor(colorSpaceId); QStringList profileNames; Q_FOREACH (const KoColorProfile *profile, profileList) { profileNames.append(profile->name()); } std::sort(profileNames.begin(), profileNames.end()); QListWidgetItem *defaultProfile = new QListWidgetItem; defaultProfile->setText(defaultProfileName + " " + i18nc("This is appended to the color profile which is the default for the given colorspace and bit-depth","(Default)")); Q_FOREACH (QString stringName, profileNames) { if (stringName == defaultProfileName) { d->colorSpaceSelector->lstProfile->addItem(defaultProfile); } else { d->colorSpaceSelector->lstProfile->addItem(stringName); } } d->colorSpaceSelector->lstProfile->setCurrentItem(defaultProfile); d->colorSpaceSelector->lstProfile->blockSignals(false); colorSpaceChanged(); } void KisAdvancedColorSpaceSelector::fillCmbDepths(const KoID& id) { KoID activeDepth = d->colorSpaceSelector->cmbColorDepth->currentItem(); d->colorSpaceSelector->cmbColorDepth->clear(); QList depths = KoColorSpaceRegistry::instance()->colorDepthList(id, KoColorSpaceRegistry::OnlyUserVisible); QList sortedDepths; if (depths.contains(Integer8BitsColorDepthID)) { sortedDepths << Integer8BitsColorDepthID; } if (depths.contains(Integer16BitsColorDepthID)) { sortedDepths << Integer16BitsColorDepthID; } if (depths.contains(Float16BitsColorDepthID)) { sortedDepths << Float16BitsColorDepthID; } if (depths.contains(Float32BitsColorDepthID)) { sortedDepths << Float32BitsColorDepthID; } if (depths.contains(Float64BitsColorDepthID)) { sortedDepths << Float64BitsColorDepthID; } d->colorSpaceSelector->cmbColorDepth->setIDList(sortedDepths); if (sortedDepths.contains(activeDepth)) { d->colorSpaceSelector->cmbColorDepth->setCurrent(activeDepth); } } void KisAdvancedColorSpaceSelector::fillDescription() { QString notApplicable = i18nc("Not Applicable, used where there's no colorants or gamma curve found","N/A"); QString notApplicableTooltip = i18nc("@info:tooltip","This profile has no colorants."); QString profileName = i18nc("Shows up instead of the name when there's no profile","No Profile Found"); QString whatIsColorant = i18n("Colorant in d50-adapted xyY."); //set colorants const QString colorSpaceId = KoColorSpaceRegistry::instance()->colorSpaceId(d->colorSpaceSelector->cmbColorModels->currentItem(), d->colorSpaceSelector->cmbColorDepth->currentItem()); QList profileList = KoColorSpaceRegistry::instance()->profilesFor(colorSpaceId); if (!profileList.isEmpty()) { profileName = currentColorSpace()->profile()->name(); if (currentColorSpace()->profile()->hasColorants()){ QVector colorants = currentColorSpace()->profile()->getColorantsxyY(); QVector whitepoint = currentColorSpace()->profile()->getWhitePointxyY(); //QString text = currentColorSpace()->profile()->info() + " =" + d->colorSpaceSelector->lblXYZ_W->setText(nameWhitePoint(whitepoint)); d->colorSpaceSelector->lblXYZ_W->setToolTip(QString::number(whitepoint[0], 'f', 4) + ", " + QString::number(whitepoint[1], 'f', 4) + ", " + QString::number(whitepoint[2], 'f', 4)); d->colorSpaceSelector->TongueWidget->setToolTip("
"+i18nc("@info:tooltip","This profile has the following xyY colorants:")+"
"+ i18n("Red:") +""+QString::number(colorants[0], 'f', 4) + "" + QString::number(colorants[1], 'f', 4) + "" + QString::number(colorants[2], 'f', 4)+"
"+ i18n("Green:")+""+QString::number(colorants[3], 'f', 4) + "" + QString::number(colorants[4], 'f', 4) + "" + QString::number(colorants[5], 'f', 4)+"
"+ i18n("Blue:") +""+QString::number(colorants[6], 'f', 4) + "" + QString::number(colorants[7], 'f', 4) + "" + QString::number(colorants[8], 'f', 4)+"
"); } else { QVector whitepoint2 = currentColorSpace()->profile()->getWhitePointxyY(); d->colorSpaceSelector->lblXYZ_W->setText(nameWhitePoint(whitepoint2)); d->colorSpaceSelector->lblXYZ_W->setToolTip(QString::number(whitepoint2[0], 'f', 4) + ", " + QString::number(whitepoint2[1], 'f', 4) + ", " + QString::number(whitepoint2[2], 'f', 4)); d->colorSpaceSelector->TongueWidget->setToolTip(notApplicableTooltip); } } else { d->colorSpaceSelector->lblXYZ_W->setText(notApplicable); d->colorSpaceSelector->lblXYZ_W->setToolTip(notApplicableTooltip); d->colorSpaceSelector->TongueWidget->setToolTip(notApplicableTooltip); } //set TRC QVector estimatedTRC(3); QString estimatedGamma = i18nc("Estimated Gamma indicates how the TRC (Tone Response Curve or Tone Reproduction Curve) is bent. A Gamma of 1.0 means linear.", "Estimated Gamma: "); QString estimatedsRGB = i18nc("This is for special Gamma types that LCMS cannot differentiate between", "Estimated Gamma: sRGB, L* or rec709 TRC"); QString whatissRGB = i18nc("@info:tooltip","The Tone Response Curve of this color space is either sRGB, L* or rec709 TRC."); QString currentModelStr = d->colorSpaceSelector->cmbColorModels->currentItem().id(); if (profileList.isEmpty()) { d->colorSpaceSelector->TongueWidget->setProfileDataAvailable(false); d->colorSpaceSelector->TRCwidget->setProfileDataAvailable(false); } else if (currentModelStr == "RGBA") { QVector colorants = currentColorSpace()->profile()->getColorantsxyY(); QVector whitepoint = currentColorSpace()->profile()->getWhitePointxyY(); if (currentColorSpace()->profile()->hasColorants()){ d->colorSpaceSelector->TongueWidget->setRGBData(whitepoint, colorants); } else { colorants.fill(0.0); d->colorSpaceSelector->TongueWidget->setRGBData(whitepoint, colorants); } d->colorSpaceSelector->TongueWidget->setGamut(currentColorSpace()->gamutXYY()); estimatedTRC = currentColorSpace()->profile()->getEstimatedTRC(); QString estimatedCurve = " Estimated curve: "; QPolygonF redcurve; QPolygonF greencurve; QPolygonF bluecurve; if (currentColorSpace()->profile()->hasTRC()){ for (int i=0; i<=10; i++) { QVector linear(3); linear.fill(i*0.1); currentColorSpace()->profile()->linearizeFloatValue(linear); estimatedCurve = estimatedCurve + ", " + QString::number(linear[0]); QPointF tonepoint(linear[0],i*0.1); redcurve<colorSpaceSelector->TRCwidget->setRGBCurve(redcurve, greencurve, bluecurve); } else { QPolygonF curve = currentColorSpace()->estimatedTRCXYY(); redcurve << curve.at(0) << curve.at(1) << curve.at(2) << curve.at(3) << curve.at(4); greencurve << curve.at(5) << curve.at(6) << curve.at(7) << curve.at(8) << curve.at(9); bluecurve << curve.at(10) << curve.at(11) << curve.at(12) << curve.at(13) << curve.at(14); d->colorSpaceSelector->TRCwidget->setRGBCurve(redcurve, greencurve, bluecurve); } if (estimatedTRC[0] == -1) { d->colorSpaceSelector->TRCwidget->setToolTip(""+whatissRGB+"
"+estimatedCurve+""); } else { d->colorSpaceSelector->TRCwidget->setToolTip(""+estimatedGamma + QString::number(estimatedTRC[0]) + "," + QString::number(estimatedTRC[1]) + "," + QString::number(estimatedTRC[2])+"
"+estimatedCurve+""); } } else if (currentModelStr == "GRAYA") { QVector whitepoint = currentColorSpace()->profile()->getWhitePointxyY(); d->colorSpaceSelector->TongueWidget->setGrayData(whitepoint); d->colorSpaceSelector->TongueWidget->setGamut(currentColorSpace()->gamutXYY()); estimatedTRC = currentColorSpace()->profile()->getEstimatedTRC(); QString estimatedCurve = " Estimated curve: "; QPolygonF tonecurve; if (currentColorSpace()->profile()->hasTRC()){ for (int i=0; i<=10; i++) { QVector linear(3); linear.fill(i*0.1); currentColorSpace()->profile()->linearizeFloatValue(linear); estimatedCurve = estimatedCurve + ", " + QString::number(linear[0]); QPointF tonepoint(linear[0],i*0.1); tonecurve<colorSpaceSelector->TRCwidget->setProfileDataAvailable(false); } d->colorSpaceSelector->TRCwidget->setGreyscaleCurve(tonecurve); if (estimatedTRC[0] == -1) { d->colorSpaceSelector->TRCwidget->setToolTip(""+whatissRGB+"
"+estimatedCurve+""); } else { d->colorSpaceSelector->TRCwidget->setToolTip(""+estimatedGamma + QString::number(estimatedTRC[0])+"
"+estimatedCurve+""); } } else if (currentModelStr == "CMYKA") { QVector whitepoint = currentColorSpace()->profile()->getWhitePointxyY(); d->colorSpaceSelector->TongueWidget->setCMYKData(whitepoint); d->colorSpaceSelector->TongueWidget->setGamut(currentColorSpace()->gamutXYY()); QString estimatedCurve = " Estimated curve: "; QPolygonF tonecurve; QPolygonF cyancurve; QPolygonF magentacurve; QPolygonF yellowcurve; if (currentColorSpace()->profile()->hasTRC()){ for (int i=0; i<=10; i++) { QVector linear(3); linear.fill(i*0.1); currentColorSpace()->profile()->linearizeFloatValue(linear); estimatedCurve = estimatedCurve + ", " + QString::number(linear[0]); QPointF tonepoint(linear[0],i*0.1); tonecurve<colorSpaceSelector->TRCwidget->setGreyscaleCurve(tonecurve); } else { QPolygonF curve = currentColorSpace()->estimatedTRCXYY(); cyancurve << curve.at(0) << curve.at(1) << curve.at(2) << curve.at(3) << curve.at(4); magentacurve << curve.at(5) << curve.at(6) << curve.at(7) << curve.at(8) << curve.at(9); yellowcurve << curve.at(10) << curve.at(11) << curve.at(12) << curve.at(13) << curve.at(14); tonecurve << curve.at(15) << curve.at(16) << curve.at(17) << curve.at(18) << curve.at(19); d->colorSpaceSelector->TRCwidget->setCMYKCurve(cyancurve, magentacurve, yellowcurve, tonecurve); } d->colorSpaceSelector->TRCwidget->setToolTip(i18nc("@info:tooltip","Estimated Gamma cannot be retrieved for CMYK.")); } else if (currentModelStr == "XYZA") { QString estimatedCurve = " Estimated curve: "; estimatedTRC = currentColorSpace()->profile()->getEstimatedTRC(); QPolygonF tonecurve; if (currentColorSpace()->profile()->hasTRC()){ for (int i=0; i<=10; i++) { QVector linear(3); linear.fill(i*0.1); currentColorSpace()->profile()->linearizeFloatValue(linear); estimatedCurve = estimatedCurve + ", " + QString::number(linear[0]); QPointF tonepoint(linear[0],i*0.1); tonecurve<colorSpaceSelector->TRCwidget->setGreyscaleCurve(tonecurve); } else { d->colorSpaceSelector->TRCwidget->setProfileDataAvailable(false); } QVector whitepoint = currentColorSpace()->profile()->getWhitePointxyY(); d->colorSpaceSelector->TongueWidget->setXYZData(whitepoint); d->colorSpaceSelector->TongueWidget->setGamut(currentColorSpace()->gamutXYY()); d->colorSpaceSelector->TRCwidget->setToolTip(""+estimatedGamma + QString::number(estimatedTRC[0])+"< br />"+estimatedCurve+""); } else if (currentModelStr == "LABA") { estimatedTRC = currentColorSpace()->profile()->getEstimatedTRC(); QString estimatedCurve = " Estimated curve: "; QPolygonF tonecurve; if (currentColorSpace()->profile()->hasTRC()){ for (int i=0; i<=10; i++) { QVector linear(3); linear.fill(i*0.1); currentColorSpace()->profile()->linearizeFloatValue(linear); estimatedCurve = estimatedCurve + ", " + QString::number(linear[0]); QPointF tonepoint(linear[0],i*0.1); tonecurve<colorSpaceSelector->TRCwidget->setGreyscaleCurve(tonecurve); } else { d->colorSpaceSelector->TRCwidget->setProfileDataAvailable(false); } QVector whitepoint = currentColorSpace()->profile()->getWhitePointxyY(); d->colorSpaceSelector->TongueWidget->setLABData(whitepoint); d->colorSpaceSelector->TongueWidget->setGamut(currentColorSpace()->gamutXYY()); d->colorSpaceSelector->TRCwidget->setToolTip(""+i18nc("@info:tooltip","This is assumed to be the L * TRC. ")+"
"+estimatedCurve+""); } else if (currentModelStr == "YCbCrA") { QVector whitepoint = currentColorSpace()->profile()->getWhitePointxyY(); d->colorSpaceSelector->TongueWidget->setYCbCrData(whitepoint); QString estimatedCurve = " Estimated curve: "; QPolygonF tonecurve; if (currentColorSpace()->profile()->hasTRC()){ for (int i=0; i<=10; i++) { QVector linear(3); linear.fill(i*0.1); currentColorSpace()->profile()->linearizeFloatValue(linear); estimatedCurve = estimatedCurve + ", " + QString::number(linear[0]); QPointF tonepoint(linear[0],i*0.1); tonecurve<colorSpaceSelector->TRCwidget->setGreyscaleCurve(tonecurve); } else { d->colorSpaceSelector->TRCwidget->setProfileDataAvailable(false); } d->colorSpaceSelector->TongueWidget->setGamut(currentColorSpace()->gamutXYY()); d->colorSpaceSelector->TRCwidget->setToolTip(i18nc("@info:tooltip","Estimated Gamma cannot be retrieved for YCrCb.")); } d->colorSpaceSelector->textProfileDescription->clear(); if (profileList.isEmpty()==false) { d->colorSpaceSelector->textProfileDescription->append("

"+i18nc("About ","About ") + currentColorSpace()->name() + "/" + profileName + "

"); d->colorSpaceSelector->textProfileDescription->append("

"+ i18nc("ICC profile version","ICC Version: ") + QString::number(currentColorSpace()->profile()->version()) + "

"); //d->colorSpaceSelector->textProfileDescription->append("

"+ i18nc("Who made the profile?","Manufacturer: ") + currentColorSpace()->profile()->manufacturer() + "

"); //This would work if people actually wrote the manufacturer into the manufacturer fiedl... d->colorSpaceSelector->textProfileDescription->append("

"+ i18nc("What is the copyright? These are from embedded strings from the icc profile, so they default to english.","Copyright: ") + currentColorSpace()->profile()->copyright() + "

"); } else { d->colorSpaceSelector->textProfileDescription->append("

" + profileName + "

"); } if (currentModelStr == "RGBA") { d->colorSpaceSelector->textProfileDescription->append("

"+i18nc("If the selected model is RGB", "RGB (Red, Green, Blue), is the color model used by screens and other light-based media.
" "RGB is an additive color model: adding colors together makes them brighter. This color " "model is the most extensive of all color models, and is recommended as a model for painting," "that you can later convert to other spaces. RGB is also the recommended colorspace for HDR editing.")+"

"); } else if (currentModelStr == "CMYKA") { d->colorSpaceSelector->textProfileDescription->append("

"+i18nc("If the selected model is CMYK", "CMYK (Cyan, Magenta, Yellow, Key), " "is the model used by printers and other ink-based media.
" "CMYK is a subtractive model, meaning that adding colors together will turn them darker. Because of CMYK " "profiles being very specific per printer, it is recommended to work in RGB space, and then later convert " "to a CMYK profile, preferably one delivered by your printer.
" "CMYK is not recommended for painting." "Unfortunately, Krita cannot retrieve colorants or the TRC for this space.")+"

"); } else if (currentModelStr == "XYZA") { d->colorSpaceSelector->textProfileDescription->append("

"+i18nc("If the selected model is XYZ", "CIE XYZ" "is the space determined by the CIE as the space that encompasses all other colors, and used to " "convert colors between profiles. XYZ is an additive color model, meaning that adding colors together " "makes them brighter. XYZ is not recommended for painting, but can be useful to encode in. The Tone Response " "Curve is assumed to be linear.")+"

"); } else if (currentModelStr == "GRAYA") { d->colorSpaceSelector->textProfileDescription->append("

"+i18nc("If the selected model is Grayscale", "Grayscale only allows for " "gray values and transparent values. Grayscale images use half " "the memory and disk space compared to an RGB image of the same bit-depth.
" "Grayscale is useful for inking and grayscale images. In " "Krita, you can mix Grayscale and RGB layers in the same image.")+"

"); } else if (currentModelStr == "LABA") { d->colorSpaceSelector->textProfileDescription->append("

"+i18nc("If the selected model is LAB", "L*a*b. L stands for Lightness, " "the a and b components represent color channels.
" "L*a*b is a special model for color correction. It is based on human perception, meaning that it " "tries to encode the difference in lightness, red-green balance and yellow-blue balance. " "This makes it useful for color correction, but the vast majority of color maths in the blending " "modes do not work as expected here.
" "Similarly, Krita does not support HDR in LAB, meaning that HDR images converted to LAB lose color " "information. This colorspace is not recommended for painting, nor for export, " "but best as a space to do post-processing in. The TRC is assumed to be the L* TRC.")+"

"); } else if (currentModelStr == "YCbCrA") { d->colorSpaceSelector->textProfileDescription->append("

"+i18nc("If the selected model is YCbCr", "YCbCr (Luma, Blue Chroma, Red Chroma), is a " "model designed for video encoding. It is based on human perception, meaning that it tries to " "encode the difference in lightness, red-green balance and yellow-blue balance. Chroma in " "this case is then a word indicating a special type of saturation, in these cases the saturation " "of Red and Blue, of which the desaturated equivalents are Green and Yellow respectively. It " "is available to open up certain images correctly, but Krita does not currently ship a profile for " "this due to lack of open source ICC profiles for YCrCb.")+"

"); } QString currentDepthStr = d->colorSpaceSelector->cmbColorDepth->currentItem().id(); if (currentDepthStr == "U8") { d->colorSpaceSelector->textProfileDescription->append("

"+i18nc("When the selected Bitdepth is 8", "8 bit integer: The default number of colors per channel. Each channel will have 256 values available, " "leading to a total amount of colors of 256 to the power of the number of channels. Recommended to use for images intended for the web, " "or otherwise simple images.")+"

"); } else if (currentDepthStr == "U16") { d->colorSpaceSelector->textProfileDescription->append("

"+i18nc("When the selected Bitdepth is 16", "16 bit integer: Also known as 'deep color'. 16 bit is ideal for editing images with a linear TRC, large " "color space, or just when you need more precise color blending. This does take twice as much space on " "the RAM and hard-drive than any given 8 bit image of the same properties, and for some devices it " "takes much more processing power. We recommend watching the RAM usage of the file carefully, or " "otherwise use 8 bit if your computer slows down. Take care to disable conversion optimization " "when converting from 16 bit/channel to 8 bit/channel.")+"

"); } else if (currentDepthStr == "F16") { d->colorSpaceSelector->textProfileDescription->append("

"+i18nc("When the selected Bitdepth is 16 bit float", "16 bit floating point: Also known as 'Half Floating Point', and the standard in VFX industry images. " "16 bit float is ideal for editing images with a linear Tone Response Curve, large color space, or just when you need " "more precise color blending. It being floating point is an absolute requirement for Scene Referred " "(HDR) images. This does take twice as much space on the RAM and hard-drive than any given 8 bit image " "of the same properties, and for some devices it takes much more processing power. We recommend watching " "the RAM usage of the file carefully, or otherwise use 8 bit if your computer slows down.")+"

"); } else if (currentDepthStr == "F32") { d->colorSpaceSelector->textProfileDescription->append("

"+i18nc("When the selected Bitdepth is 32bit float", "32 bit float point: Also known as 'Full Floating Point'. 32 bit float is ideal for editing images " "with a linear TRC, large color space, or just when you need more precise color blending. It being " "floating point is an absolute requirement for Scene Referred (HDR) images. This does take four times " "as much space on the RAM and hard-drive than any given 8 bit image of the same properties, and for " "some devices it takes much more processing power. We recommend watching the RAM usage of the file " "carefully, or otherwise use 8 bit if your computer slows down.")+"

"); } else if (currentDepthStr == "F64") { d->colorSpaceSelector->textProfileDescription->append("

"+i18nc("When the selected Bitdepth is 64bit float, but this isn't actually available in Krita at the moment.",\ "64 bit float point: 64 bit float is as precise as it gets in current technology, and this depth is used " "most of the time for images that are generated or used as an input for software. It being floating point " "is an absolute requirement for Scene Referred (HDR) images. This does take eight times as much space on " "the RAM and hard-drive than any given 8 bit image of the same properties, and for some devices it takes " "much more processing power. We recommend watching the RAM usage of the file carefully, or otherwise use " "8 bit if your computer slows down.")+"

"); } if (profileList.isEmpty()==false) { QString possibleConversionIntents = "

"+i18n("The following conversion intents are possible: ")+"

    "; if (currentColorSpace()->profile()->supportsPerceptual()){ possibleConversionIntents += "
  • "+i18n("Perceptual")+"
  • "; } if (currentColorSpace()->profile()->supportsRelative()){ possibleConversionIntents += "
  • "+i18n("Relative Colorimetric")+"
  • "; } if (currentColorSpace()->profile()->supportsAbsolute()){ possibleConversionIntents += "
  • "+i18n("Absolute Colorimetric")+"
  • "; } if (currentColorSpace()->profile()->supportsSaturation()){ possibleConversionIntents += "
  • "+i18n("Saturation")+"
  • "; } possibleConversionIntents += "

"; d->colorSpaceSelector->textProfileDescription->append(possibleConversionIntents); } if (profileName.contains("-elle-")) { d->colorSpaceSelector->textProfileDescription->append("

"+i18nc("These are Elle Stone's notes on her profiles that we ship.", "

Extra notes on profiles by Elle Stone:

" "

Krita comes with a number of high quality profiles created by " "Elle Stone. This is a summary. Please check " "the full documentation as well.

")); if (profileName.contains("ACES-")) { d->colorSpaceSelector->textProfileDescription->append(i18nc("From Elle's notes.", "

Quoting Wikipedia, 'Academy Color Encoding System (ACES) is a color image " "encoding system proposed by the Academy of Motion Picture Arts and Sciences that will allow for " "a fully encompassing color accurate workflow, with 'seamless interchange of high quality motion " "picture images regardless of source'.

")); } if (profileName.contains("ACEScg-")) { d->colorSpaceSelector->textProfileDescription->append(i18nc("From Elle's notes.", "

The ACEScg color space is smaller than the ACES color space, but large enough to contain the 'Rec-2020 gamut " "and the DCI-P3 gamut', unlike the ACES color space it has no negative values and contains only few colors " "that fall just barely outside the area of real colors humans can see

")); } if (profileName.contains("ClayRGB-")) { d->colorSpaceSelector->textProfileDescription->append(i18nc("From Elle's notes.", "

To avoid possible copyright infringement issues, I used 'ClayRGB' (following ArgyllCMS) as the base name " "for these profiles. As used below, 'Compatible with Adobe RGB 1998' is terminology suggested in the preamble " "to the AdobeRGB 1998 color space specifications.

" "The Adobe RGB 1998 color gamut covers a higher " "percentage of real-world cyans, greens, and yellow-greens than sRGB, but still doesn't include all printable " "cyans, greens, yellow-greens, especially when printing using today's high-end, wider gamut, ink jet printers. " "BetaRGB (not included in the profile pack) and Rec.2020 are better matches for the color gamuts of today's " "wide gamut printers.

" "The Adobe RGB 1998 color gamut is a reasonable approximation to some of today's " "high-end wide gamut monitors.

")); } if (profileName.contains("AllColorsRGB-")) { d->colorSpaceSelector->textProfileDescription->append(i18nc("From Elle's notes.", "

This profile's color gamut is roughly the same size and shape as the ACES color space gamut, " "and like the ACES color space, AllColorsRGB holds all possible real colors. But AllColorsRGB " "actually has a slightly larger color gamut (to capture some fringe colors that barely qualify " "as real when viewed by the standard observer) and uses the D50 white point.

" "Just like the ACES color space, AllColorsRGB holds a high percentage of imaginary colors. See the Completely " "" "Painless Programmer's Guide to XYZ, RGB, ICC, xyY, and TRCs for more information about imaginary " "colors.

" "There is no particular reason why anyone would want to use this profile " "for editing, unless one needs to make sure your color space really does hold all " "possible real colors.

")); } if (profileName.contains("CIERGB-")) { d->colorSpaceSelector->textProfileDescription->append(i18nc("From Elle's notes.", "

This profile is included mostly for its historical significance. " "It's the color space that was used in the original color matching experiments " "that led to the creation of the XYZ reference color space.

" "The ASTM E white point " "is probably the right E white point to use when making the CIERGB color space profile. " "It's not clear to me what the correct CIERGB primaries really are. " "Lindbloom gives one set. The LCMS version 1 tutorial gives a different set. " "Experts in the field contend that the real primaries " "should be calculated from the spectral wavelengths, so I did.

")); } if (profileName.contains("IdentityRGB-")) { d->colorSpaceSelector->textProfileDescription->append(i18nc("From Elle's notes.", "

The IdentityRGB working space is included in the profile pack because it's a mathematically " "obvious way to include all possible visible colors, though it has a higher percentage of " "imaginary colors than the ACES and AllColorsRGB color spaces. I cannot think of any reason " "why you'd ever want to actually edit images in the IdentityRGB working space.

")); } if (profileName.contains("LargeRGB-")) { d->colorSpaceSelector->textProfileDescription->append(i18nc("From Elle's notes.", "

To avoid possible copyright infringement issues, I used 'LargeRGB' (following RawTherapee) " "as the base name for these profiles.

" "Kodak designed the RIMM/ROMM (ProPhotoRGB) color " "gamut to include all printable and most real world colors. It includes some imaginary colors " "and excludes some of the real world blues and violet blues that can be captured by digital " "cameras. It also excludes some very saturated 'camera-captured' yellows as interpreted by " "some (and probably many) camera matrix input profiles.

" "The ProPhotoRGB primaries are " "hard-coded into Adobe products such as Lightroom and the Dng-DCP camera 'profiles'. However, " "other than being large enough to hold a lot of colors, ProPhotoRGB has no particular merit " "as an RGB working space. Personally I recommend the Rec.2020 or ACEScg profiles over " "ProPhotoRGB. But if you have an already well-established workflow using ProPhotoRGB, you " "might find a shift to another RGB working space a little odd, at least at first, and so you " "have to weight the pros and cons of changing your workflow.

")); } if (profileName.contains("Rec2020-")) { d->colorSpaceSelector->textProfileDescription->append(i18nc("From Elle's notes.", "

Rec.2020 is the up-and-coming replacement for the thoroughly outdated sRGB color space. As of " "June 2015, very few (if any) display devices (and certainly no affordable display devices) can " "display all of Rec.2020. However, display technology is closing in on Rec.2020, movies are " "already being made for Rec.2020, and various cameras offer support for Rec.2020. And in the " "digital darkroom Rec.2020 is much more suitable as a general RGB working space than the " "exceedingly small sRGB color space.

")); } if (profileName.contains("sRGB-")) { d->colorSpaceSelector->textProfileDescription->append(i18nc("From Elle's notes.", "

Hewlett-Packard and Microsoft designed sRGB to match the color gamut of consumer-grade CRTs " "from the 1990s. sRGB is the standard color space for the world wide web and is still the best " "choice for exporting images to the internet.

" "The sRGB color gamut was a good match to " "calibrated decent quality CRTs. But sRGB is not a good match to many consumer-grade LCD monitors, " "which often cannot display the more saturated sRGB blues and magentas (the good news: as technology " "progresses, wider gamuts are trickling down to consumer grade monitors).

" "Printer color gamuts can easily exceed the sRGB color gamut in cyans, greens, and yellow-greens. Colors from interpolated " "camera raw files also often exceed the sRGB color gamut.

" "As a very relevant aside, using perceptual " "intent when converting to sRGB does not magically makes otherwise out of gamut colors fit inside the " "sRGB color gamut! The standard sRGB color space (along with all the other the RGB profiles provided " "in my profile pack) is a matrix profile, and matrix profiles don't have perceptual intent tables.

")); } if (profileName.contains("WideRGB-")) { d->colorSpaceSelector->textProfileDescription->append(i18nc("From Elle's notes.", "

To avoid possible copyright infringement issues, I used 'WideRGB' as the base name for these profiles.

" "WideGamutRGB was designed by Adobe to be a wide gamut color space that uses spectral colors " "as its primaries. Pascale's primary values produce a profile that matches old V2 Widegamut profiles " "from Adobe and Canon. It is an interesting color space, but shortly after its introduction, Adobe " "switched their emphasis to the ProPhotoRGB color space.

")); } if (profileName.contains("Gray-")) { d->colorSpaceSelector->textProfileDescription->append(i18nc("From Elle's notes.", "

These profiles are for use with RGB images that have been converted to monotone gray (black and white). " "The main reason to convert from RGB to Gray is to save the file space needed to encode the image. " "Google places a premium on fast-loading web pages, and images are one of the slower-loading elements " "of a web page. So converting black and white images to Grayscale images does save some kilobytes. " " For grayscale images uploaded to the internet, convert the image to the V2 Gray profile with the sRGB TRC.

")); } if (profileName.contains("-g10")) { d->colorSpaceSelector->textProfileDescription->append(i18nc("From Elle's notes.", "

The profiles that end in '-g10.icc' are linear gamma (gamma=1.0, 'linear light', etc) profiles and " "should only be used when editing at high bit depths (16-bit floating point, 16-bit integer, 32-bit " "floating point, 32-bit integer). Many editing operations produce better results in linear gamma color " "spaces.

")); } if (profileName.contains("-labl")) { d->colorSpaceSelector->textProfileDescription->append(i18nc("From Elle's notes.", "

The profiles that end in '-labl.icc' have perceptually uniform TRCs. A few editing operations really " "should be done on perceptually uniform RGB. Make sure you use the V4 versions for editing high bit depth " "images.

")); } if (profileName.contains("-srgbtrc") || profileName.contains("-g22") || profileName.contains("-g18") || profileName.contains("-rec709")) { d->colorSpaceSelector->textProfileDescription->append(i18nc("From Elle's notes.", "

The profiles that end in '-srgbtrc.icc', '-g22.icc', and '-rec709.icc' have approximately but not exactly " "perceptually uniform TRCs. ProPhotoRGB's gamma=1.8 TRC is not quite as close to being perceptually uniform.

")); } if (d->colorSpaceSelector->cmbColorDepth->currentItem().id()=="U8") { d->colorSpaceSelector->textProfileDescription->append(i18nc("From Elle's notes.", "

When editing 8-bit images, you should use a profile with a small color gamut and an approximately or " "exactly perceptually uniform TRC. Of the profiles supplied in my profile pack, only the sRGB and AdobeRGB1998 " "(ClayRGB) color spaces are small enough for 8-bit editing. Even with the AdobeRGB1998 color space you need to " "be careful to not cause posterization. And of course you cannot use the linear gamma versions of these profiles " "for 8-bit editing.

")); } if (profileName.contains("-V4-")) { d->colorSpaceSelector->textProfileDescription->append(i18nc("From Elle's notes.", "

Use V4 profiles for editing images using high bit depth image editors that use LCMS as the Color Management Module. " "This includes Krita, digiKam/showFoto, and GIMP 2.9.

")); } if (profileName.contains("-V2-")) { d->colorSpaceSelector->textProfileDescription->append(i18nc("From Elle's notes.", "

Use V2 profiles for exporting finished images to be uploaded to the web or for use with imaging software that " "cannot read V4 profiles.

")); } } d->colorSpaceSelector->textProfileDescription->moveCursor(QTextCursor::Start); } QString KisAdvancedColorSpaceSelector::nameWhitePoint(QVector whitePoint) { QString name=(QString::number(whitePoint[0]) + ", " + QString::number(whitePoint[1], 'f', 4)); //A (0.451170, 0.40594) (2856K)(tungsten) if ((whitePoint[0]>0.451170-0.005 && whitePoint[0]<0.451170 + 0.005) && (whitePoint[1]>0.40594-0.005 && whitePoint[1]<0.40594 + 0.005)){ name="A"; return name; } //B (0.34980, 0.35270) (4874K) (Direct Sunlight at noon)(obsolete) - //C (0.31039, 0.31905) (6774K) (avarage/north sky daylight)(obsolete) + //C (0.31039, 0.31905) (6774K) (average/north sky daylight)(obsolete) //D50 (0.34773, 0.35952) (5003K) (Horizon Light, default color of white paper, ICC profile standard illuminant) if ((whitePoint[0]>0.34773-0.005 && whitePoint[0]<0.34773 + 0.005) && (whitePoint[1]>0.35952-0.005 && whitePoint[1]<0.35952 + 0.005)){ name="D50"; return name; } //D55 (0.33411, 0.34877) (5503K) (Mid-morning / Mid-afternoon Daylight) if ((whitePoint[0]>0.33411-0.001 && whitePoint[0]<0.33411 + 0.001) && (whitePoint[1]>0.34877-0.005 && whitePoint[1]<0.34877 + 0.005)){ name="D55"; return name; } //D60 (0.3217, 0.3378) (~6000K) (ACES colorspace default) if ((whitePoint[0]>0.3217-0.001 && whitePoint[0]<0.3217 + 0.001) && (whitePoint[1]>0.3378-0.005 && whitePoint[1]<0.3378 + 0.005)){ name="D60"; return name; } //D65 (0.31382, 0.33100) (6504K) (Noon Daylight, default for computer and tv screens, sRGB default) //Elle's are old school with 0.3127 and 0.3289 if ((whitePoint[0]>0.31382-0.002 && whitePoint[0]<0.31382 + 0.002) && (whitePoint[1]>0.33100-0.005 && whitePoint[1]<0.33100 + 0.002)){ name="D65"; return name; } //D75 (0.29968, 0.31740) (7504K) (North sky Daylight) if ((whitePoint[0]>0.29968-0.001 && whitePoint[0]<0.29968 + 0.001) && (whitePoint[1]>0.31740-0.005 && whitePoint[1]<0.31740 + 0.005)){ name="D75"; return name; } //E (1/3, 1/3) (5454K) (Equal Energy. CIERGB default) if ((whitePoint[0]>(1.0/3.0)-0.001 && whitePoint[0]<(1.0/3.0) + 0.001) && (whitePoint[1]>(1.0/3.0)-0.001 && whitePoint[1]<(1.0/3.0) + 0.001)){ name="E"; return name; } //The F series seems to sorta overlap with the D series, so I'll just leave them in comment here.// //F1 (0.31811, 0.33559) (6430K) (Daylight Fluorescent) //F2 (0.37925, 0.36733) (4230K) (Cool White Fluorescent) //F3 (0.41761, 0.38324) (3450K) (White Fluorescent) //F4 (0.44920, 0.39074) (2940K) (Warm White Fluorescent) //F5 (0.31975, 0.34246) (6350K) (Daylight Fluorescent) //F6 (0.38660, 0.37847) (4150K) (Lite White Fluorescent) //F7 (0.31569, 0.32960) (6500K) (D65 simulator, Daylight simulator) //F8 (0.34902, 0.35939) (5000K) (D50 simulator) //F9 (0.37829, 0.37045) (4150K) (Cool White Deluxe Fluorescent) //F10 (0.35090, 0.35444) (5000K) (Philips TL85, Ultralume 50) //F11 (0.38541, 0.37123) (4000K) (Philips TL84, Ultralume 40) //F12 (0.44256, 0.39717) (3000K) (Philips TL83, Ultralume 30) return name; } const KoColorSpace* KisAdvancedColorSpaceSelector::currentColorSpace() { QString check = ""; if (d->colorSpaceSelector->lstProfile->currentItem()) { check = d->colorSpaceSelector->lstProfile->currentItem()->text(); } else if (d->colorSpaceSelector->lstProfile->item(0)) { check = d->colorSpaceSelector->lstProfile->item(0)->text(); } return KoColorSpaceRegistry::instance()->colorSpace(d->colorSpaceSelector->cmbColorModels->currentItem().id(), d->colorSpaceSelector->cmbColorDepth->currentItem().id(), check); } void KisAdvancedColorSpaceSelector::setCurrentColorModel(const KoID& id) { d->colorSpaceSelector->cmbColorModels->setCurrent(id); fillLstProfiles(); fillCmbDepths(id); } void KisAdvancedColorSpaceSelector::setCurrentColorDepth(const KoID& id) { d->colorSpaceSelector->cmbColorDepth->setCurrent(id); fillLstProfiles(); } void KisAdvancedColorSpaceSelector::setCurrentProfile(const QString& name) { QList Items= d->colorSpaceSelector->lstProfile->findItems(name, Qt::MatchStartsWith); d->colorSpaceSelector->lstProfile->setCurrentItem(Items.at(0)); } void KisAdvancedColorSpaceSelector::setCurrentColorSpace(const KoColorSpace* colorSpace) { if (!colorSpace) { return; } setCurrentColorModel(colorSpace->colorModelId()); setCurrentColorDepth(colorSpace->colorDepthId()); setCurrentProfile(colorSpace->profile()->name()); } void KisAdvancedColorSpaceSelector::colorSpaceChanged() { bool valid = d->colorSpaceSelector->lstProfile->count() != 0; emit(selectionChanged(valid)); if (valid) { emit colorSpaceChanged(currentColorSpace()); } } void KisAdvancedColorSpaceSelector::installProfile() { KoFileDialog dialog(this, KoFileDialog::OpenFiles, "OpenDocumentICC"); dialog.setCaption(i18n("Install Color Profiles")); dialog.setDefaultDir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation)); dialog.setMimeTypeFilters(QStringList() << "application/vnd.iccprofile", "application/vnd.iccprofile"); QStringList profileNames = dialog.filenames(); KoColorSpaceEngine *iccEngine = KoColorSpaceEngineRegistry::instance()->get("icc"); Q_ASSERT(iccEngine); QString saveLocation = KoResourcePaths::saveLocation("icc_profiles"); Q_FOREACH (const QString &profileName, profileNames) { QUrl file(profileName); if (!QFile::copy(profileName, saveLocation + file.fileName())) { dbgKrita << "Could not install profile!"; return; } iccEngine->addProfile(saveLocation + file.fileName()); } fillLstProfiles(); } diff --git a/libs/ui/widgets/kis_curve_widget.cpp b/libs/ui/widgets/kis_curve_widget.cpp index 1c5292d21f..286f74cb0a 100644 --- a/libs/ui/widgets/kis_curve_widget.cpp +++ b/libs/ui/widgets/kis_curve_widget.cpp @@ -1,552 +1,552 @@ /* * Copyright (c) 2005 C. Boemann * 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. */ // C++ includes. #include #include // Qt includes. #include #include #include #include #include #include #include #include #include #include #include #include #include #include // KDE includes. #include #include #include #include #include // Local includes. #include "widgets/kis_curve_widget.h" #define bounds(x,a,b) (xb ? b :x)) #define MOUSE_AWAY_THRES 15 #define POINT_AREA 1E-4 #define CURVE_AREA 1E-4 #include "kis_curve_widget_p.h" KisCurveWidget::KisCurveWidget(QWidget *parent, Qt::WindowFlags f) : QWidget(parent, f), d(new KisCurveWidget::Private(this)) { setObjectName("KisCurveWidget"); d->m_grab_point_index = -1; d->m_readOnlyMode = false; d->m_guideVisible = false; d->m_pixmapDirty = true; d->m_pixmapCache = 0; d->setState(ST_NORMAL); d->m_intIn = 0; d->m_intOut = 0; connect(&d->m_modifiedSignalsCompressor, SIGNAL(timeout()), SLOT(notifyModified())); connect(this, SIGNAL(compressorShouldEmitModified()), SLOT(slotCompressorShouldEmitModified())); setMouseTracking(true); setAutoFillBackground(false); setAttribute(Qt::WA_OpaquePaintEvent); setMinimumSize(150, 50); setMaximumSize(250, 250); setFocusPolicy(Qt::StrongFocus); } KisCurveWidget::~KisCurveWidget() { delete d->m_pixmapCache; delete d; } void KisCurveWidget::setupInOutControls(QSpinBox *in, QSpinBox *out, int inMin, int inMax, int outMin, int outMax) { dropInOutControls(); d->m_intIn = in; d->m_intOut = out; if (!d->m_intIn || !d->m_intOut) return; d->m_inMin = inMin; d->m_inMax = inMax; d->m_outMin = outMin; d->m_outMax = outMax; int realInMin = qMin(inMin, inMax); // tilt elevation has range (90, 0), which QSpinBox can't handle int realInMax = qMax(inMin, inMax); d->m_intIn->setRange(realInMin, realInMax); d->m_intOut->setRange(d->m_outMin, d->m_outMax); connect(d->m_intIn, SIGNAL(valueChanged(int)), this, SLOT(inOutChanged(int)), Qt::UniqueConnection); connect(d->m_intOut, SIGNAL(valueChanged(int)), this, SLOT(inOutChanged(int)), Qt::UniqueConnection); d->syncIOControls(); } void KisCurveWidget::dropInOutControls() { if (!d->m_intIn || !d->m_intOut) return; disconnect(d->m_intIn, SIGNAL(valueChanged(int)), this, SLOT(inOutChanged(int))); disconnect(d->m_intOut, SIGNAL(valueChanged(int)), this, SLOT(inOutChanged(int))); d->m_intIn = d->m_intOut = 0; } void KisCurveWidget::inOutChanged(int) { QPointF pt; Q_ASSERT(d->m_grab_point_index >= 0); pt.setX(d->io2sp(d->m_intIn->value(), d->m_inMin, d->m_inMax)); pt.setY(d->io2sp(d->m_intOut->value(), d->m_outMin, d->m_outMax)); bool newPoint = false; if (d->jumpOverExistingPoints(pt, d->m_grab_point_index)) { newPoint = true; d->m_curve.setPoint(d->m_grab_point_index, pt); d->m_grab_point_index = d->m_curve.points().indexOf(pt); emit pointSelectedChanged(); } else { pt = d->m_curve.points()[d->m_grab_point_index]; } if (!newPoint) { // if there is a new Point, no point in rewriting values in spinboxes d->m_intIn->blockSignals(true); d->m_intOut->blockSignals(true); d->m_intIn->setValue(d->sp2io(pt.x(), d->m_inMin, d->m_inMax)); d->m_intOut->setValue(d->sp2io(pt.y(), d->m_outMin, d->m_outMax)); d->m_intIn->blockSignals(false); d->m_intOut->blockSignals(false); } d->setCurveModified(false); } void KisCurveWidget::reset(void) { d->m_grab_point_index = -1; emit pointSelectedChanged(); d->m_guideVisible = false; //remove total - 2 points. while (d->m_curve.points().count() - 2 ) { d->m_curve.removePoint(d->m_curve.points().count() - 2); } d->setCurveModified(); } void KisCurveWidget::setCurveGuide(const QColor & color) { d->m_guideVisible = true; d->m_colorGuide = color; } void KisCurveWidget::setPixmap(const QPixmap & pix) { d->m_pix = pix; d->m_pixmapDirty = true; d->setCurveRepaint(); } QPixmap KisCurveWidget::getPixmap() { return d->m_pix; } void KisCurveWidget::setBasePixmap(const QPixmap &pix) { d->m_pixmapBase = pix; } QPixmap KisCurveWidget::getBasePixmap() { return d->m_pixmapBase; } bool KisCurveWidget::pointSelected() const { return d->m_grab_point_index > 0 && d->m_grab_point_index < d->m_curve.points().count() - 1; } void KisCurveWidget::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Delete || e->key() == Qt::Key_Backspace) { if (d->m_grab_point_index > 0 && d->m_grab_point_index < d->m_curve.points().count() - 1) { //x() find closest point to get focus afterwards double grab_point_x = d->m_curve.points()[d->m_grab_point_index].x(); int left_of_grab_point_index = d->m_grab_point_index - 1; int right_of_grab_point_index = d->m_grab_point_index + 1; int new_grab_point_index; if (fabs(d->m_curve.points()[left_of_grab_point_index].x() - grab_point_x) < fabs(d->m_curve.points()[right_of_grab_point_index].x() - grab_point_x)) { new_grab_point_index = left_of_grab_point_index; } else { new_grab_point_index = d->m_grab_point_index; } d->m_curve.removePoint(d->m_grab_point_index); d->m_grab_point_index = new_grab_point_index; emit pointSelectedChanged(); setCursor(Qt::ArrowCursor); d->setState(ST_NORMAL); } e->accept(); d->setCurveModified(); } else if (e->key() == Qt::Key_Escape && d->state() != ST_NORMAL) { d->m_curve.setPoint(d->m_grab_point_index, QPointF(d->m_grabOriginalX, d->m_grabOriginalY) ); setCursor(Qt::ArrowCursor); d->setState(ST_NORMAL); e->accept(); d->setCurveModified(); } else if ((e->key() == Qt::Key_A || e->key() == Qt::Key_Insert) && d->state() == ST_NORMAL) { /* FIXME: Lets user choose the hotkeys */ addPointInTheMiddle(); e->accept(); } else QWidget::keyPressEvent(e); } void KisCurveWidget::addPointInTheMiddle() { QPointF pt(0.5, d->m_curve.value(0.5)); if (!d->jumpOverExistingPoints(pt, -1)) return; d->m_grab_point_index = d->m_curve.addPoint(pt); emit pointSelectedChanged(); if (d->m_intIn) d->m_intIn->setFocus(Qt::TabFocusReason); d->setCurveModified(); } void KisCurveWidget::resizeEvent(QResizeEvent *e) { d->m_pixmapDirty = true; QWidget::resizeEvent(e); } void KisCurveWidget::paintEvent(QPaintEvent *) { int wWidth = width() - 1; int wHeight = height() - 1; QPainter p(this); // Antialiasing is not a good idea here, because // the grid will drift one pixel to any side due to rounding of int // FIXME: let's user tell the last word (in config) //p.setRenderHint(QPainter::Antialiasing); QPalette appPalette = QApplication::palette(); p.fillRect(rect(), appPalette.color(QPalette::Base)); // clear out previous paint call results // make the entire widget grayed out if it is disabled if (!this->isEnabled()) { p.setOpacity(0.2); } // draw background if (!d->m_pix.isNull()) { if (d->m_pixmapDirty || !d->m_pixmapCache) { delete d->m_pixmapCache; d->m_pixmapCache = new QPixmap(width(), height()); QPainter cachePainter(d->m_pixmapCache); cachePainter.scale(1.0*width() / d->m_pix.width(), 1.0*height() / d->m_pix.height()); cachePainter.drawPixmap(0, 0, d->m_pix); d->m_pixmapDirty = false; } p.drawPixmap(0, 0, *d->m_pixmapCache); } d->drawGrid(p, wWidth, wHeight); KisConfig cfg(true); if (cfg.antialiasCurves()) { p.setRenderHint(QPainter::Antialiasing); } // Draw curve. double curY; double normalizedX; int x; QPolygonF poly; p.setPen(QPen(appPalette.color(QPalette::Text), 2, Qt::SolidLine)); for (x = 0 ; x < wWidth ; x++) { normalizedX = double(x) / wWidth; curY = wHeight - d->m_curve.value(normalizedX) * wHeight; /** * Keep in mind that QLineF rounds doubles * to ints mathematically, not just rounds down * like in C */ poly.append(QPointF(x, curY)); } poly.append(QPointF(x, wHeight - d->m_curve.value(1.0) * wHeight)); p.drawPolyline(poly); QPainterPath fillCurvePath; QPolygonF fillPoly = poly; fillPoly.append(QPoint(rect().width(), rect().height())); fillPoly.append(QPointF(0,rect().height())); // add a couple points to the edges so it fills in below always QColor fillColor = appPalette.color(QPalette::Text); fillColor.setAlphaF(0.2); fillCurvePath.addPolygon(fillPoly); p.fillPath(fillCurvePath, fillColor); // Drawing curve handles. double curveX; double curveY; if (!d->m_readOnlyMode) { for (int i = 0; i < d->m_curve.points().count(); ++i) { curveX = d->m_curve.points().at(i).x(); curveY = d->m_curve.points().at(i).y(); - int handleSize = 12; // how big should control points be (diamater size) + int handleSize = 12; // how big should control points be (diameter size) if (i == d->m_grab_point_index) { // active point is slightly more "bold" p.setPen(QPen(appPalette.color(QPalette::Text), 4, Qt::SolidLine)); p.drawEllipse(QRectF(curveX * wWidth - (handleSize*0.5), wHeight - (handleSize*0.5) - curveY * wHeight, handleSize, handleSize)); } else { p.setPen(QPen(appPalette.color(QPalette::Text), 2, Qt::SolidLine)); p.drawEllipse(QRectF(curveX * wWidth - (handleSize*0.5), wHeight - (handleSize*0.5) - curveY * wHeight, handleSize, handleSize)); } } } // add border around widget to help contain everything QPainterPath widgetBoundsPath; widgetBoundsPath.addRect(rect()); p.strokePath(widgetBoundsPath, appPalette.color(QPalette::Text)); p.setOpacity(1.0); // reset to 1.0 in case we were drawing a disabled widget before } void KisCurveWidget::mousePressEvent(QMouseEvent * e) { if (d->m_readOnlyMode) return; if (e->button() != Qt::LeftButton) return; double x = e->pos().x() / (double)(width() - 1); double y = 1.0 - e->pos().y() / (double)(height() - 1); int closest_point_index = d->nearestPointInRange(QPointF(x, y), width(), height()); if (closest_point_index < 0) { QPointF newPoint(x, y); if (!d->jumpOverExistingPoints(newPoint, -1)) return; d->m_grab_point_index = d->m_curve.addPoint(newPoint); emit pointSelectedChanged(); } else { d->m_grab_point_index = closest_point_index; emit pointSelectedChanged(); } d->m_grabOriginalX = d->m_curve.points()[d->m_grab_point_index].x(); d->m_grabOriginalY = d->m_curve.points()[d->m_grab_point_index].y(); d->m_grabOffsetX = d->m_curve.points()[d->m_grab_point_index].x() - x; d->m_grabOffsetY = d->m_curve.points()[d->m_grab_point_index].y() - y; d->m_curve.setPoint(d->m_grab_point_index, QPointF(x + d->m_grabOffsetX, y + d->m_grabOffsetY)); d->m_draggedAwayPointIndex = -1; d->setState(ST_DRAG); d->setCurveModified(); } void KisCurveWidget::mouseReleaseEvent(QMouseEvent *e) { if (d->m_readOnlyMode) return; if (e->button() != Qt::LeftButton) return; setCursor(Qt::ArrowCursor); d->setState(ST_NORMAL); d->setCurveModified(); } void KisCurveWidget::mouseMoveEvent(QMouseEvent * e) { if (d->m_readOnlyMode) return; double x = e->pos().x() / (double)(width() - 1); double y = 1.0 - e->pos().y() / (double)(height() - 1); if (d->state() == ST_NORMAL) { // If no point is selected set the cursor shape if on top int nearestPointIndex = d->nearestPointInRange(QPointF(x, y), width(), height()); if (nearestPointIndex < 0) setCursor(Qt::ArrowCursor); else setCursor(Qt::CrossCursor); } else { // Else, drag the selected point bool crossedHoriz = e->pos().x() - width() > MOUSE_AWAY_THRES || e->pos().x() < -MOUSE_AWAY_THRES; bool crossedVert = e->pos().y() - height() > MOUSE_AWAY_THRES || e->pos().y() < -MOUSE_AWAY_THRES; bool removePoint = (crossedHoriz || crossedVert); if (!removePoint && d->m_draggedAwayPointIndex >= 0) { // point is no longer dragged away so reinsert it QPointF newPoint(d->m_draggedAwayPoint); d->m_grab_point_index = d->m_curve.addPoint(newPoint); d->m_draggedAwayPointIndex = -1; } if (removePoint && (d->m_draggedAwayPointIndex >= 0)) return; setCursor(Qt::CrossCursor); x += d->m_grabOffsetX; y += d->m_grabOffsetY; double leftX; double rightX; if (d->m_grab_point_index == 0) { leftX = 0.0; if (d->m_curve.points().count() > 1) rightX = d->m_curve.points()[d->m_grab_point_index + 1].x() - POINT_AREA; else rightX = 1.0; } else if (d->m_grab_point_index == d->m_curve.points().count() - 1) { leftX = d->m_curve.points()[d->m_grab_point_index - 1].x() + POINT_AREA; rightX = 1.0; } else { Q_ASSERT(d->m_grab_point_index > 0 && d->m_grab_point_index < d->m_curve.points().count() - 1); // the 1E-4 addition so we can grab the dot later. leftX = d->m_curve.points()[d->m_grab_point_index - 1].x() + POINT_AREA; rightX = d->m_curve.points()[d->m_grab_point_index + 1].x() - POINT_AREA; } x = bounds(x, leftX, rightX); y = bounds(y, 0., 1.); d->m_curve.setPoint(d->m_grab_point_index, QPointF(x, y)); if (removePoint && d->m_curve.points().count() > 2) { d->m_draggedAwayPoint = d->m_curve.points()[d->m_grab_point_index]; d->m_draggedAwayPointIndex = d->m_grab_point_index; d->m_curve.removePoint(d->m_grab_point_index); d->m_grab_point_index = bounds(d->m_grab_point_index, 0, d->m_curve.points().count() - 1); emit pointSelectedChanged(); } d->setCurveModified(); } } KisCubicCurve KisCurveWidget::curve() { return d->m_curve; } void KisCurveWidget::setCurve(KisCubicCurve inlist) { d->m_curve = inlist; d->m_grab_point_index = qBound(0, d->m_grab_point_index, d->m_curve.points().count() - 1); d->setCurveModified(); emit pointSelectedChanged(); } void KisCurveWidget::leaveEvent(QEvent *) { } void KisCurveWidget::notifyModified() { emit modified(); } void KisCurveWidget::slotCompressorShouldEmitModified() { d->m_modifiedSignalsCompressor.start(); } diff --git a/libs/ui/widgets/kis_paintop_presets_popup.h b/libs/ui/widgets/kis_paintop_presets_popup.h index fea64cc57d..0222ae9337 100644 --- a/libs/ui/widgets/kis_paintop_presets_popup.h +++ b/libs/ui/widgets/kis_paintop_presets_popup.h @@ -1,147 +1,147 @@ /* This file is part of the KDE project * Copyright (C) Boudewijn Rempt , (C) 2008 * Copyright (C) 2010 Lukáš Tvrdý * Copyright (C) 2011 Silvio Heinrich * * 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_PAINTOP_PRESETS_POPUP_H #define KIS_PAINTOP_PRESETS_POPUP_H #include #include #include #include #include #include "../kis_paint_ops_model.h" #include #include #include "widgets/kis_paintop_presets_popup.h" #include "kis_favorite_resource_manager.h" class QString; class KisCanvasResourceProvider; class KoResource; /** * Popup widget for presets with built-in functionality * for adding and removing presets. */ class KisPaintOpPresetsPopup : public QWidget { Q_OBJECT public: KisPaintOpPresetsPopup(KisCanvasResourceProvider * resourceProvider, KisFavoriteResourceManager* favoriteResourceManager, KisPresetSaveWidget* savePresetWidget, QWidget * parent = 0); ~KisPaintOpPresetsPopup() override; void setPaintOpSettingsWidget(QWidget * widget); ///Image for preset preview ///@return image cut out from the scratchpad QImage cutOutOverlay(); void setPaintOpList(const QList& list); void setCurrentPaintOpId(const QString & paintOpId); /// returns the internal ID for the paint op (brush engine) QString currentPaintOpId(); - ///fill the cutoutOverlay rect with the cotent of an image, used to get the image back when selecting a preset + ///fill the cutoutOverlay rect with the content of an image, used to get the image back when selecting a preset ///@param image image that will be used, should be image of an existing preset resource void setPresetImage(const QImage& image); void resizeEvent(QResizeEvent* ) override; bool detached() const; void updateViewSettings(); void currentPresetChanged(KisPaintOpPresetSP preset); KisPresetSaveWidget * saveDialog; // toggle the state when we are creating a brush from scratch void setCreatingBrushFromScratch(bool enable); protected: void contextMenuEvent(QContextMenuEvent *) override; void hideEvent(QHideEvent *) override; void showEvent(QShowEvent *) override; public Q_SLOTS: void switchDetached(bool show = true); void resourceSelected(KoResource* resource); void updateThemedIcons(); void slotUpdatePresetSettings(); void slotUpdateLodAvailability(); void slotRenameBrushActivated(); void slotRenameBrushDeactivated(); void slotSaveRenameCurrentBrush(); void slotCreateNewBrushPresetEngine(); Q_SIGNALS: void savePresetClicked(); void saveBrushPreset(); void defaultPresetClicked(); void paintopActivated(const QString& presetName); void signalResourceSelected(KoResource* resource); void reloadPresetClicked(); void dirtyPresetToggled(bool value); void eraserBrushSizeToggled(bool value); void eraserBrushOpacityToggled(bool value); void brushEditorShown(); void createPresetFromScratch(const QString& paintOpName); private Q_SLOTS: void slotSwitchScratchpad(bool visible); void slotResourceChanged(int key, const QVariant &value); void slotLodAvailabilityChanged(bool value); void slotLodThresholdChanged(qreal value); void slotSwitchShowEditor(bool visible); void slotUpdatePaintOpFilter(); void slotSwitchShowPresets(bool visible); void slotSaveBrushPreset(); void slotSaveNewBrushPreset(); /// we do not delete brushe presets, but blacklist them so they disappear from the interface void slotBlackListCurrentPreset(); private: struct Private; Private * const m_d; QString current_paintOpId; QList sortedBrushEnginesList; QMenu * newPresetBrushEnginesMenu; QList newBrushEngineOptions; void toggleBrushRenameUIActive(bool isRenaming); }; #endif diff --git a/libs/widgets/KisDlgInternalColorSelector.cpp b/libs/widgets/KisDlgInternalColorSelector.cpp index 825aa4e77d..f228e43b6f 100644 --- a/libs/widgets/KisDlgInternalColorSelector.cpp +++ b/libs/widgets/KisDlgInternalColorSelector.cpp @@ -1,354 +1,354 @@ /* * Copyright (C) Wolthera van Hovell tot Westerflier , (C) 2016 * * 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 #include #include #include #include #include #include #include "KoColorSpaceRegistry.h" #include #include #include #include #include #include #include "kis_signal_compressor.h" #include "KoColorDisplayRendererInterface.h" #include "kis_spinbox_color_selector.h" #include "KisDlgInternalColorSelector.h" #include "ui_WdgDlgInternalColorSelector.h" #include "kis_config_notifier.h" #include "kis_color_input.h" #include "kis_icon_utils.h" #include "KisSqueezedComboBox.h" std::function KisDlgInternalColorSelector::s_screenColorPickerFactory = 0; struct KisDlgInternalColorSelector::Private { bool allowUpdates = true; KoColor currentColor; KoColor previousColor; KoColor sRGB = KoColor(KoColorSpaceRegistry::instance()->rgb8()); const KoColorSpace *currentColorSpace; bool lockUsedCS = false; bool chooseAlpha = false; KisSignalCompressor *compressColorChanges; const KoColorDisplayRendererInterface *displayRenderer; KisHexColorInput *hexColorInput = 0; KisPaletteModel *paletteModel = 0; KisPaletteListWidget *paletteChooser = 0; KisScreenColorPickerBase *screenColorPicker = 0; }; KisDlgInternalColorSelector::KisDlgInternalColorSelector(QWidget *parent, KoColor color, Config config, const QString &caption, const KoColorDisplayRendererInterface *displayRenderer) : QDialog(parent) , m_d(new Private) { setModal(config.modal); setFocusPolicy(Qt::ClickFocus); m_ui = new Ui_WdgDlgInternalColorSelector(); m_ui->setupUi(this); setWindowTitle(caption); m_d->currentColor = color; m_d->currentColorSpace = m_d->currentColor.colorSpace(); m_d->displayRenderer = displayRenderer; m_ui->spinboxselector->slotSetColor(color); connect(m_ui->spinboxselector, SIGNAL(sigNewColor(KoColor)), this, SLOT(slotColorUpdated(KoColor))); m_ui->visualSelector->slotSetColor(color); m_ui->visualSelector->setDisplayRenderer(displayRenderer); m_ui->visualSelector->setConfig(false, config.modal); if (config.visualColorSelector) { connect(m_ui->visualSelector, SIGNAL(sigNewColor(KoColor)), this, SLOT(slotColorUpdated(KoColor))); connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), m_ui->visualSelector, SLOT(configurationChanged())); } else { m_ui->visualSelector->hide(); } m_d->paletteChooser = new KisPaletteListWidget(this); m_d->paletteModel = new KisPaletteModel(this); m_ui->bnPaletteChooser->setIcon(KisIconUtils::loadIcon("hi16-palette_library")); m_ui->paletteBox->setPaletteModel(m_d->paletteModel); m_ui->paletteBox->setDisplayRenderer(displayRenderer); m_ui->cmbNameList->setCompanionView(m_ui->paletteBox); connect(m_d->paletteChooser, SIGNAL(sigPaletteSelected(KoColorSet*)), this, SLOT(slotChangePalette(KoColorSet*))); connect(m_ui->cmbNameList, SIGNAL(sigColorSelected(KoColor)), SLOT(slotColorUpdated(KoColor))); - // For some bizare reason, the modal dialog doesn't like having the colorset set, so let's not. + // For some bizarre reason, the modal dialog doesn't like having the colorset set, so let's not. if (config.paletteBox) { //TODO: Add disable signal as well. Might be not necessary...? KConfigGroup cfg(KSharedConfig::openConfig()->group("")); QString paletteName = cfg.readEntry("internal_selector_active_color_set", QString()); KoResourceServer* rServer = KoResourceServerProvider::instance()->paletteServer(); KoColorSet *savedPal = rServer->resourceByName(paletteName); if (savedPal) { this->slotChangePalette(savedPal); } else { if (rServer->resources().count()) { savedPal = rServer->resources().first(); if (savedPal) { this->slotChangePalette(savedPal); } } } connect(m_ui->paletteBox, SIGNAL(sigColorSelected(KoColor)), this, SLOT(slotColorUpdated(KoColor))); m_ui->bnPaletteChooser->setPopupWidget(m_d->paletteChooser); } else { m_ui->paletteBox->setEnabled(false); m_ui->cmbNameList->setEnabled(false); m_ui->bnPaletteChooser->setEnabled(false); } if (config.prevNextButtons) { m_ui->currentColor->setColor(m_d->currentColor); m_ui->currentColor->setDisplayRenderer(displayRenderer); m_ui->previousColor->setColor(m_d->currentColor); m_ui->previousColor->setDisplayRenderer(displayRenderer); connect(m_ui->previousColor, SIGNAL(triggered(KoColorPatch*)), SLOT(slotSetColorFromPatch(KoColorPatch*))); } else { m_ui->currentColor->hide(); m_ui->previousColor->hide(); } if (config.hexInput) { m_d->sRGB.fromKoColor(m_d->currentColor); m_d->hexColorInput = new KisHexColorInput(this, &m_d->sRGB); m_d->hexColorInput->update(); connect(m_d->hexColorInput, SIGNAL(updated()), SLOT(slotSetColorFromHex())); m_ui->rightPane->addWidget(m_d->hexColorInput); m_d->hexColorInput->setToolTip(i18n("This is a hexcode input, for webcolors. It can only get colors in the sRGB space.")); } // KisScreenColorPicker is in the kritaui module, so dependency inversion is used to access it. m_ui->screenColorPickerWidget->setLayout(new QHBoxLayout(m_ui->screenColorPickerWidget)); if (s_screenColorPickerFactory) { m_d->screenColorPicker = s_screenColorPickerFactory(m_ui->screenColorPickerWidget); m_ui->screenColorPickerWidget->layout()->addWidget(m_d->screenColorPicker); if (config.screenColorPicker) { connect(m_d->screenColorPicker, SIGNAL(sigNewColorPicked(KoColor)),this, SLOT(slotColorUpdated(KoColor))); } else { m_d->screenColorPicker->hide(); } } m_d->compressColorChanges = new KisSignalCompressor(100 /* ms */, KisSignalCompressor::POSTPONE, this); connect(m_d->compressColorChanges, SIGNAL(timeout()), this, SLOT(endUpdateWithNewColor())); connect(m_ui->buttonBox, SIGNAL(accepted()), this, SLOT(accept()), Qt::UniqueConnection); connect(m_ui->buttonBox, SIGNAL(rejected()), this, SLOT(reject()), Qt::UniqueConnection); connect(this, SIGNAL(finished(int)), SLOT(slotFinishUp())); } KisDlgInternalColorSelector::~KisDlgInternalColorSelector() { delete m_ui; } void KisDlgInternalColorSelector::slotColorUpdated(KoColor newColor) { // not-so-nice solution: if someone calls this slot directly and that code was // triggered by our compressor signal, our compressor is technically the sender()! if (sender() == m_d->compressColorChanges) { return; } // Do not accept external updates while a color update emit is pending; // Note: Assumes external updates only come from parent(), a separate slot might be better if (m_d->allowUpdates || (QObject::sender() && QObject::sender() != this->parent())) { // Enforce palette colors KConfigGroup group(KSharedConfig::openConfig(), ""); if (group.readEntry("colorsettings/forcepalettecolors", false)) { newColor = m_ui->paletteBox->closestColor(newColor); } if (m_d->lockUsedCS){ newColor.convertTo(m_d->currentColorSpace); m_d->currentColor = newColor; } else { m_d->currentColor = newColor; } updateAllElements(QObject::sender()); } } void KisDlgInternalColorSelector::slotSetColorFromPatch(KoColorPatch *patch) { slotColorUpdated(patch->color()); } void KisDlgInternalColorSelector::colorSpaceChanged(const KoColorSpace *cs) { if (cs == m_d->currentColorSpace) { return; } m_d->currentColorSpace = KoColorSpaceRegistry::instance()->colorSpace(cs->colorModelId().id(), cs->colorDepthId().id(), cs->profile()); m_ui->spinboxselector->slotSetColorSpace(m_d->currentColorSpace); m_ui->visualSelector->slotsetColorSpace(m_d->currentColorSpace); } void KisDlgInternalColorSelector::lockUsedColorSpace(const KoColorSpace *cs) { colorSpaceChanged(cs); if (m_d->currentColor.colorSpace() != m_d->currentColorSpace) { m_d->currentColor.convertTo(m_d->currentColorSpace); } m_d->lockUsedCS = true; } void KisDlgInternalColorSelector::setDisplayRenderer(const KoColorDisplayRendererInterface *displayRenderer) { if (displayRenderer) { m_d->displayRenderer = displayRenderer; m_ui->visualSelector->setDisplayRenderer(displayRenderer); m_ui->currentColor->setDisplayRenderer(displayRenderer); m_ui->previousColor->setDisplayRenderer(displayRenderer); m_ui->paletteBox->setDisplayRenderer(displayRenderer); } else { m_d->displayRenderer = KoDumbColorDisplayRenderer::instance(); } } KoColor KisDlgInternalColorSelector::getModalColorDialog(const KoColor color, QWidget* parent, QString caption) { Config config = Config(); KisDlgInternalColorSelector dialog(parent, color, config, caption); dialog.setPreviousColor(color); dialog.exec(); return dialog.getCurrentColor(); } KoColor KisDlgInternalColorSelector::getCurrentColor() { return m_d->currentColor; } void KisDlgInternalColorSelector::chooseAlpha(bool chooseAlpha) { m_d->chooseAlpha = chooseAlpha; } void KisDlgInternalColorSelector::slotConfigurationChanged() { //m_d->canvas->displayColorConverter()-> //slotColorSpaceChanged(m_d->canvas->image()->colorSpace()); } void KisDlgInternalColorSelector::setPreviousColor(KoColor c) { m_d->previousColor = c; } void KisDlgInternalColorSelector::reject() { slotColorUpdated(m_d->previousColor); QDialog::reject(); } void KisDlgInternalColorSelector::updateAllElements(QObject *source) { //update everything!!! if (source != m_ui->spinboxselector) { m_ui->spinboxselector->slotSetColor(m_d->currentColor); } if (source != m_ui->visualSelector) { m_ui->visualSelector->slotSetColor(m_d->currentColor); } if (source != m_d->hexColorInput) { m_d->sRGB.fromKoColor(m_d->currentColor); m_d->hexColorInput->update(); } if (source != m_ui->paletteBox) { m_ui->paletteBox->selectClosestColor(m_d->currentColor); } m_ui->previousColor->setColor(m_d->previousColor); m_ui->currentColor->setColor(m_d->currentColor); if (source && source != this->parent()) { m_d->allowUpdates = false; m_d->compressColorChanges->start(); } if (m_d->screenColorPicker) { m_d->screenColorPicker->updateIcons(); } } void KisDlgInternalColorSelector::endUpdateWithNewColor() { emit signalForegroundColorChosen(m_d->currentColor); m_d->allowUpdates = true; } void KisDlgInternalColorSelector::focusInEvent(QFocusEvent *) { //setPreviousColor(); } void KisDlgInternalColorSelector::slotFinishUp() { setPreviousColor(m_d->currentColor); KConfigGroup cfg(KSharedConfig::openConfig()->group("")); if (m_d->paletteModel) { if (m_d->paletteModel->colorSet()) { cfg.writeEntry("internal_selector_active_color_set", m_d->paletteModel->colorSet()->name()); } } } void KisDlgInternalColorSelector::slotSetColorFromHex() { slotColorUpdated(m_d->sRGB); } void KisDlgInternalColorSelector::slotChangePalette(KoColorSet *set) { if (!set) { return; } m_d->paletteModel->setPalette(set); } void KisDlgInternalColorSelector::showEvent(QShowEvent *event) { updateAllElements(0); QDialog::showEvent(event); } diff --git a/libs/widgets/KoResourceItemChooserSync.h b/libs/widgets/KoResourceItemChooserSync.h index 7bfd213657..e85e112324 100644 --- a/libs/widgets/KoResourceItemChooserSync.h +++ b/libs/widgets/KoResourceItemChooserSync.h @@ -1,65 +1,65 @@ /* This file is part of the KDE project Copyright (c) 2014 Sven Langkamp 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 KORESOURCEITEMCHOOSERSYNC_H #define KORESOURCEITEMCHOOSERSYNC_H #include #include #include "kritawidgets_export.h" /** * KoResourceItemChooserSync is a singleton that syncs the size of entries in the * resource item choosers between different choosers * To use the syncing it has to be turned on in the KoResourceItemChooser */ class KRITAWIDGETS_EXPORT KoResourceItemChooserSync : public QObject { Q_OBJECT public: KoResourceItemChooserSync(); ~KoResourceItemChooserSync() override; static KoResourceItemChooserSync* instance(); /// Gets the base length /// @returns the base length of items int baseLength(); /// Set the base length - /// @param length base length for the items, will be clamped if ouside range + /// @param length base length for the items, will be clamped if outside range void setBaseLength(int length); Q_SIGNALS: /// Signal is emitted when the base length is changed and will trigger and update in /// the resource item choosers void baseLengthChanged(int length); private: KoResourceItemChooserSync(const KoResourceItemChooserSync&); KoResourceItemChooserSync operator=(const KoResourceItemChooserSync&); private: struct Private; const QScopedPointer d; }; #endif // KORESOURCEITEMCHOOSERSYNC_H diff --git a/libs/widgetutils/KisActionsSnapshot.h b/libs/widgetutils/KisActionsSnapshot.h index b938dc648b..68dfb52f45 100644 --- a/libs/widgetutils/KisActionsSnapshot.h +++ b/libs/widgetutils/KisActionsSnapshot.h @@ -1,62 +1,62 @@ /* * Copyright (c) 2016 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 KISACTIONSSNAPSHOT_H #define KISACTIONSSNAPSHOT_H #include #include #include class QAction; class KActionCollection; /** * @brief The KisActionsSnapshot class */ class KRITAWIDGETUTILS_EXPORT KisActionsSnapshot { public: KisActionsSnapshot(); ~KisActionsSnapshot(); /** * @brief registers the action in the snapshot and sorts it into a proper * category. The action is *not* owned by the snapshot. * * @param name id string of the action * @param action the action itself */ void addAction(const QString &name, QAction *action); /** * Returns all action collections of the current snapshot * - * WARNING: the collections are owned by the shapshot! Don't destroy + * WARNING: the collections are owned by the snapshot! Don't destroy * the snapshot before you are done with the collections! */ QMap actionCollections(); private: struct Private; const QScopedPointer m_d; }; #endif // KISACTIONSSNAPSHOT_H diff --git a/libs/widgetutils/KoGroupButton.h b/libs/widgetutils/KoGroupButton.h index 7519c4b471..fd31710391 100644 --- a/libs/widgetutils/KoGroupButton.h +++ b/libs/widgetutils/KoGroupButton.h @@ -1,71 +1,71 @@ /* This file is part of the KDE libraries Copyright (C) 2007 Aurélien Gâteau Copyright (C) 2012 Jean-Nicolas Artaud Copyright (C) 2012 Jarosław Staniek This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 KOGROUPBUTTON_H #define KOGROUPBUTTON_H #include "kritawidgetutils_export.h" #include /** * A thin tool button which can be visually grouped with other buttons. * * The group can thus look like one solid bar: ( button1 | button2 | button3 ) * - * For groupping layout can be used. For exclusive checkable behaviour assign QButtonGroup on the buttons. + * For grouping layout can be used. For exclusive checkable behaviour assign QButtonGroup on the buttons. */ class KRITAWIDGETUTILS_EXPORT KoGroupButton : public KisHighlightedToolButton { Q_OBJECT Q_ENUMS( GroupPosition ) Q_PROPERTY( GroupPosition groupPosition READ groupPosition WRITE setGroupPosition ) public: /** * Position of the button within the button group what affects the appearance. */ enum GroupPosition { NoGroup, //!< No particular position, gives the button unchanged appearance GroupLeft, //!< The button is at the left of the group, so it would have rounded the left part GroupRight, //!< The button is at the right of the group, so it would have rounded the right part GroupCenter //!< The button is on the center of the group, so it would have separators on both sides }; explicit KoGroupButton(GroupPosition position, QWidget* parent = 0); /** * Creates button with no NoGroup position. */ explicit KoGroupButton(QWidget* parent = 0); ~KoGroupButton() override; void setGroupPosition(KoGroupButton::GroupPosition groupPosition); KoGroupButton::GroupPosition groupPosition() const; protected: void paintEvent(QPaintEvent* event) override; private: class Private; Private *const d; }; #endif /* KOGROUPBUTTON_H */ diff --git a/libs/widgetutils/KoProgressUpdater.cpp b/libs/widgetutils/KoProgressUpdater.cpp index 65822e0a48..9451f0d4b5 100644 --- a/libs/widgetutils/KoProgressUpdater.cpp +++ b/libs/widgetutils/KoProgressUpdater.cpp @@ -1,348 +1,348 @@ /* This file is part of the KDE project * Copyright (C) 2006-2007 Thomas Zander * Copyright (C) 2009 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 "KoProgressUpdater.h" #include #include #include #include #include "KoUpdaterPrivate_p.h" #include "KoUpdater.h" #include "KoProgressProxy.h" #include class Q_DECL_HIDDEN KoProgressUpdater::Private { public: Private(KoProgressUpdater *_q, KoProgressProxy *proxy, QPointer parentUpdater, Mode _mode) : q(_q) , parentProgressProxy(proxy) , parentUpdater(parentUpdater) , mode(_mode) , currentProgress(0) , updated(false) , updateGuiTimer(_q) , canceled(false) { } KoProgressUpdater *q; private: KoProgressProxy *parentProgressProxy; QPointer parentUpdater; public: Mode mode; int currentProgress = 0; bool isUndefinedState = false; bool updated; // is true whenever the progress needs to be recomputed QTimer updateGuiTimer; // fires regularly to update the progress bar widget QList > subtasks; bool canceled; int updateInterval = 250; // ms, 4 updates per second should be enough bool autoNestNames = false; QString taskName; int taskMax = -1; bool isStarted = false; void updateParentText(); void clearState(); KoProgressProxy* progressProxy() { return parentUpdater ? parentUpdater : parentProgressProxy; } }; // NOTE: do not make the KoProgressUpdater object part of the QObject // hierarchy. Do not make KoProgressProxy its parent (note that KoProgressProxy // is not necessarily castable to QObject ). This prevents proper functioning // of progress reporting in multi-threaded environments. KoProgressUpdater::KoProgressUpdater(KoProgressProxy *progressProxy, Mode mode) : d (new Private(this, progressProxy, 0, mode)) { KIS_ASSERT_RECOVER_RETURN(progressProxy); connect(&d->updateGuiTimer, SIGNAL(timeout()), SLOT(updateUi())); } KoProgressUpdater::KoProgressUpdater(QPointer updater) : d (new Private(this, 0, updater, Unthreaded)) { KIS_ASSERT_RECOVER_RETURN(updater); connect(&d->updateGuiTimer, SIGNAL(timeout()), SLOT(updateUi())); } KoProgressUpdater::~KoProgressUpdater() { if (d->progressProxy()) { d->progressProxy()->setRange(0, d->taskMax); d->progressProxy()->setValue(d->progressProxy()->maximum()); } // make sure to stop the timer to avoid accessing // the data we are going to delete right now d->updateGuiTimer.stop(); qDeleteAll(d->subtasks); d->subtasks.clear(); delete d; } void KoProgressUpdater::start(int range, const QString &text) { d->clearState(); d->taskName = text; d->taskMax = range - 1; d->isStarted = true; if (d->progressProxy()) { d->progressProxy()->setRange(0, d->taskMax); d->progressProxy()->setValue(0); d->updateParentText(); } d->updateGuiTimer.start(d->updateInterval); } QPointer KoProgressUpdater::startSubtask(int weight, const QString &name, bool isPersistent) { if (!d->isStarted) { // lazy initialization for intermediate proxies start(); } KoUpdaterPrivate *p = new KoUpdaterPrivate(this, weight, name, isPersistent); d->subtasks.append(p); connect(p, SIGNAL(sigUpdated()), SLOT(update())); QPointer updater = p->connectedUpdater(); if (!d->updateGuiTimer.isActive()) { // we maybe need to restart the timer if it was stopped in updateUi() cause // other sub-tasks created before this one finished already. d->updateGuiTimer.start(d->updateInterval); } d->updated = true; return updater; } void KoProgressUpdater::removePersistentSubtask(QPointer updater) { for (auto it = d->subtasks.begin(); it != d->subtasks.end();) { if ((*it)->connectedUpdater() != updater) { ++it; } else { KIS_SAFE_ASSERT_RECOVER_NOOP((*it)->isPersistent()); (*it)->deleteLater(); it = d->subtasks.erase(it); break; } } updateUi(); } void KoProgressUpdater::cancel() { Q_FOREACH (KoUpdaterPrivate *updater, d->subtasks) { updater->setProgress(100); updater->setInterrupted(true); } d->canceled = true; updateUi(); } void KoProgressUpdater::update() { d->updated = true; if (d->mode == Unthreaded) { qApp->processEvents(); } if (!d->updateGuiTimer.isActive()) { d->updateGuiTimer.start(d->updateInterval); } } void KoProgressUpdater::updateUi() { // This function runs in the app main thread. All the progress // updates arrive at the KoUpdaterPrivate instances through - // queued connections, so until we relinguish control to the + // queued connections, so until we relinquish control to the // event loop, the progress values cannot change, and that // won't happen until we return from this function (which is // triggered by a timer) /** * We shouldn't let progress updater to interfere the progress * reporting when it is not initialized. */ if (d->subtasks.isEmpty()) return; if (d->updated) { int totalProgress = 0; int totalWeight = 0; d->isUndefinedState = false; Q_FOREACH (QPointer updater, d->subtasks) { if (updater->interrupted()) { d->currentProgress = -1; break; } if (!updater->hasValidRange()) { totalWeight = 0; totalProgress = 0; d->isUndefinedState = true; break; } if (updater->isPersistent() && updater->isCompleted()) { continue; } const int progress = qBound(0, updater->progress(), 100); totalProgress += progress * updater->weight(); totalWeight += updater->weight(); } const int progressPercent = totalWeight > 0 ? totalProgress / totalWeight : -1; d->currentProgress = d->taskMax == 99 ? progressPercent : qRound(qreal(progressPercent) * d->taskMax / 99.0); d->updated = false; } if (d->progressProxy()) { if (!d->isUndefinedState) { d->progressProxy()->setRange(0, d->taskMax); if (d->currentProgress == -1) { d->currentProgress = d->progressProxy()->maximum(); } if (d->currentProgress >= d->progressProxy()->maximum()) { // we're done d->updateGuiTimer.stop(); d->clearState(); } else { d->progressProxy()->setValue(d->currentProgress); } } else { d->progressProxy()->setRange(0,0); d->progressProxy()->setValue(0); } d->updateParentText(); } } void KoProgressUpdater::Private::updateParentText() { if (!progressProxy()) return; QString actionName = taskName; if (autoNestNames) { Q_FOREACH (QPointer updater, subtasks) { if (updater->isPersistent() && updater->isCompleted()) { continue; } if (updater->progress() < 100) { const QString subTaskName = updater->mergedSubTaskName(); if (!subTaskName.isEmpty()) { if (actionName.isEmpty()) { actionName = subTaskName; } else { actionName = QString("%1: %2").arg(actionName).arg(subTaskName); } } break; } } progressProxy()->setAutoNestedName(actionName); } else { progressProxy()->setFormat(actionName); } } void KoProgressUpdater::Private::clearState() { for (auto it = subtasks.begin(); it != subtasks.end();) { if (!(*it)->isPersistent()) { (*it)->deleteLater(); it = subtasks.erase(it); } else { if ((*it)->interrupted()) { (*it)->setInterrupted(false); } ++it; } } progressProxy()->setRange(0, taskMax); progressProxy()->setValue(progressProxy()->maximum()); canceled = false; } bool KoProgressUpdater::interrupted() const { return d->canceled; } void KoProgressUpdater::setUpdateInterval(int ms) { d->updateInterval = ms; if (d->updateGuiTimer.isActive()) { d->updateGuiTimer.start(d->updateInterval); } } int KoProgressUpdater::updateInterval() const { return d->updateInterval; } void KoProgressUpdater::setAutoNestNames(bool value) { d->autoNestNames = value; update(); } bool KoProgressUpdater::autoNestNames() const { return d->autoNestNames; } diff --git a/libs/widgetutils/xmlgui/KisShortcutsEditor_p.h b/libs/widgetutils/xmlgui/KisShortcutsEditor_p.h index ec63653b57..084764444e 100644 --- a/libs/widgetutils/xmlgui/KisShortcutsEditor_p.h +++ b/libs/widgetutils/xmlgui/KisShortcutsEditor_p.h @@ -1,189 +1,189 @@ /** * Copyright (C) 1997 Nicolas Hadacek * Copyright (C) 1998 Matthias Ettrich * Copyright (C) 2001 Ellis Whitehead * Copyright (C) 2006 Hamish Rodda * Copyright (C) 2007 Roberto Raggi * Copyright (C) 2007 Andreas Hartmetz * Copyright (C) 2008 Michael Jansen * Copyright (c) 2015 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 3 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 KISSHORTCUTSEDITOR_P #define KISSHORTCUTSEDITOR_P #include "KisShortcutsEditor.h" #include #include #include #include "KisShortcutsDialog_p.h" // NEEDED FOR KisShortcutsEditorPrivate #include "ui_KisShortcutsDialog.h" /// Declared at the end of the file - provides the basic storage unit for this class class KisShortcutsEditorItem; /** * @internal */ class KisShortcutsEditorPrivate { public: //! Represents the three hierarchies the dialog displays. enum hierarchyLevel {Root = 0, /* Base level node (Tools, Krita...) */ Program, /* We use this like "Path Tool, Default Tool," */ Action}; /* Individual actions */ KisShortcutsEditorPrivate(KisShortcutsEditor *q); void initGUI(KisShortcutsEditor::ActionTypes actionTypes, KisShortcutsEditor::LetterShortcuts allowLetterShortcuts); void appendToView(uint nList, const QString &title = QString()); //used in appendToView QTreeWidgetItem *findOrMakeItem(QTreeWidgetItem *parent, const QString &name); // Set all shortcuts to their default values (bindings). void allDefault(); // clear all shortcuts void clearConfiguration(); //changeXXX were described as "conflict resolution functions" void changeKeyShortcut(KisShortcutsEditorItem *item, uint column, const QKeySequence &capture); //this invokes the appropriate conflict resolution function void capturedShortcut(const QVariant &, const QModelIndex &); /** - * Add @p action at hierchy level @p level. + * Add @p action at hierarchy level @p level. * * Filters out QActions (TODO: hmm) and unnamed actions before adding. * * @return true if the actions was successfully added */ bool addAction(QAction *action, QTreeWidgetItem *hier[], hierarchyLevel level); void printShortcuts() const; // TODO: Is this necessary w/o global actions? void setActionTypes(KisShortcutsEditor::ActionTypes actionTypes); public: // Members QList actionCollections; KisShortcutsEditor *q; Ui::KisShortcutsDialog ui; KisShortcutsEditor::ActionTypes actionTypes; KisShortcutsEditorDelegate *delegate; }; // Hack to make two protected methods public. // Used by both KisShortcutsEditorPrivate and KisShortcutsEditorDelegate class QTreeWidgetHack : public QTreeWidget { public: QTreeWidgetItem *itemFromIndex(const QModelIndex &index) const { return QTreeWidget::itemFromIndex(index); } QModelIndex indexFromItem(QTreeWidgetItem *item, int column) const { return QTreeWidget::indexFromItem(item, column); } }; /** * A QTreeWidgetItem that can handle QActions. It also provides undo functionality. * * Call commit() to save pending changes. * * @internal */ class KisShortcutsEditorItem : public QTreeWidgetItem { public: KisShortcutsEditorItem(QTreeWidgetItem *parent, QAction *action); //! Destructor will erase unsaved changes. ~KisShortcutsEditorItem() override; //! Undo the changes since the last commit. void undo(); //! Commit the changes. void commit(); QVariant data(int column, int role = Qt::DisplayRole) const override; bool operator<(const QTreeWidgetItem &other) const override; QKeySequence keySequence(uint column) const; void setKeySequence(uint column, const QKeySequence &seq); bool isModified(uint column) const; bool isModified() const; void setNameBold(bool flag) { m_isNameBold = flag; } private: friend class KisShortcutsEditorPrivate; //! Recheck modified status - could have changed back to initial value void updateModified(); //! The action this item is responsible for QAction *m_action; //! Should the Name column be painted in bold? bool m_isNameBold{false}; //@{ //! The original shortcuts before user changes. 0 means no change. QList *m_oldLocalShortcut{0}; //@} //! The localized action name QString m_actionNameInTable; //! The action id. Needed for exporting and importing QString m_id; //! The collator, for sorting QCollator m_collator; }; #endif // KISSHORTCUTSEDITOR_P diff --git a/libs/widgetutils/xmlgui/kcheckaccelerators.cpp b/libs/widgetutils/xmlgui/kcheckaccelerators.cpp index 58cdc83ed0..7a3a2cca5b 100644 --- a/libs/widgetutils/xmlgui/kcheckaccelerators.cpp +++ b/libs/widgetutils/xmlgui/kcheckaccelerators.cpp @@ -1,317 +1,317 @@ /* This file is part of the KDE libraries Copyright (C) 1997 Matthias Kalle Dalheimer (kalle@kde.org) Copyright (C) 1998, 1999, 2000 KDE Team Copyright (C) 2008 Nick Shaforostoff 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 "kcheckaccelerators.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include class KCheckAcceleratorsInitializer : public QObject { Q_OBJECT public: explicit KCheckAcceleratorsInitializer(QObject *parent = 0) : QObject(parent) { } public Q_SLOTS: void initiateIfNeeded() { KConfigGroup cg(KSharedConfig::openConfig(), "Development"); QString sKey = cg.readEntry("CheckAccelerators").trimmed(); int key = 0; if (!sKey.isEmpty()) { QList cuts = QKeySequence::listFromString(sKey); if (!cuts.isEmpty()) { key = cuts.first()[0]; } } const bool autoCheck = cg.readEntry("AutoCheckAccelerators", true); const bool copyWidgetText = cg.readEntry("CopyWidgetText", false); if (!copyWidgetText && key == 0 && !autoCheck) { deleteLater(); return; } new KCheckAccelerators(qApp, key, autoCheck, copyWidgetText); deleteLater(); } }; static void startupFunc() { // Call initiateIfNeeded once we're in the event loop // This is to prevent using KSharedConfig before main() can set the app name QCoreApplication *app = QCoreApplication::instance(); KCheckAcceleratorsInitializer *initializer = new KCheckAcceleratorsInitializer(app); QMetaObject::invokeMethod(initializer, "initiateIfNeeded", Qt::QueuedConnection); } Q_COREAPP_STARTUP_FUNCTION(startupFunc) KCheckAccelerators::KCheckAccelerators(QObject *parent, int key_, bool autoCheck_, bool copyWidgetText_) : QObject(parent) , key(key_) , block(false) , autoCheck(autoCheck_) , copyWidgetText(copyWidgetText_) , drklash(0) { setObjectName(QStringLiteral("kapp_accel_filter")); KConfigGroup cg(KSharedConfig::openConfig(), "Development"); alwaysShow = cg.readEntry("AlwaysShowCheckAccelerators", false); copyWidgetTextCommand = cg.readEntry("CopyWidgetTextCommand", QString()); parent->installEventFilter(this); connect(&autoCheckTimer, SIGNAL(timeout()), SLOT(autoCheckSlot())); } bool KCheckAccelerators::eventFilter(QObject *obj, QEvent *e) { if (block) { return false; } - switch (e->type()) { // just simplify debuggin + switch (e->type()) { // just simplify debugging case QEvent::ShortcutOverride: if (key && (static_cast(e)->key() == key)) { block = true; checkAccelerators(false); block = false; e->accept(); return true; } break; case QEvent::ChildAdded: case QEvent::ChildRemoved: // Only care about widgets; this also avoids starting the timer in other threads if (!static_cast(e)->child()->isWidgetType()) { break; } Q_FALLTHROUGH(); case QEvent::Resize: case QEvent::LayoutRequest: case QEvent::WindowActivate: case QEvent::WindowDeactivate: if (autoCheck) { autoCheckTimer.setSingleShot(true); autoCheckTimer.start(20); // 20 ms } break; //case QEvent::MouseButtonDblClick: case QEvent::MouseButtonPress: if (copyWidgetText && static_cast(e)->button() == Qt::MidButton) { //kWarning()<<"obj"<(obj)->childAt(static_cast(e)->pos()); if (!w) { w = static_cast(obj); } if (!w) { return false; } //kWarning()<<"MouseButtonDblClick"<(w)) { text = static_cast(w)->text(); } else if (qobject_cast(w)) { text = static_cast(w)->text(); } else if (qobject_cast(w)) { text = static_cast(w)->currentText(); } else if (qobject_cast(w)) { text = static_cast(w)->tabText(static_cast(w)->tabAt(static_cast(e)->pos())); } else if (qobject_cast(w)) { text = static_cast(w)->title(); } else if (qobject_cast(obj)) { QAction *a = static_cast(obj)->actionAt(static_cast(e)->pos()); if (!a) { return false; } text = a->text(); if (text.isEmpty()) { text = a->iconText(); } } if (text.isEmpty()) { return false; } if (static_cast(e)->modifiers() == Qt::ControlModifier) { text.remove(QChar::fromLatin1('&')); } //kWarning()<setText(text); } else { QProcess *script = new QProcess(this); script->start(copyWidgetTextCommand.arg(text).arg(QFile::decodeName(KLocalizedString::applicationDomain()))); connect(script, SIGNAL(finished(int,QProcess::ExitStatus)), script, SLOT(deleteLater())); } e->accept(); return true; //kWarning()<<"MouseButtonDblClick"<(obj)->childAt(static_cast(e)->globalPos()); } return false; case QEvent::Timer: case QEvent::MouseMove: case QEvent::Paint: return false; default: // qDebug() << "KCheckAccelerators::eventFilter " << e->type() << " " << autoCheck; break; } return false; } void KCheckAccelerators::autoCheckSlot() { if (block) { autoCheckTimer.setSingleShot(true); autoCheckTimer.start(20); return; } block = true; checkAccelerators(!alwaysShow); block = false; } void KCheckAccelerators::createDialog(QWidget *actWin, bool automatic) { if (drklash) { return; } drklash = new QDialog(actWin); drklash->setAttribute(Qt::WA_DeleteOnClose); drklash->setObjectName(QStringLiteral("kapp_accel_check_dlg")); drklash->setWindowTitle(i18nc("@title:window", "Dr. Klash' Accelerator Diagnosis")); drklash->resize(500, 460); QVBoxLayout *layout = new QVBoxLayout(drklash); drklash_view = new QTextBrowser(drklash); layout->addWidget(drklash_view); QCheckBox *disableAutoCheck = 0; if (automatic) { disableAutoCheck = new QCheckBox(i18nc("@option:check", "Disable automatic checking"), drklash); connect(disableAutoCheck, SIGNAL(toggled(bool)), SLOT(slotDisableCheck(bool))); layout->addWidget(disableAutoCheck); } QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, drklash); layout->addWidget(buttonBox); connect(buttonBox, SIGNAL(rejected()), drklash, SLOT(close())); if (disableAutoCheck) { disableAutoCheck->setFocus(); } else { drklash_view->setFocus(); } } void KCheckAccelerators::slotDisableCheck(bool on) { autoCheck = !on; if (!on) { autoCheckSlot(); } } void KCheckAccelerators::checkAccelerators(bool automatic) { QWidget *actWin = qApp->activeWindow(); if (!actWin) { return; } KAcceleratorManager::manage(actWin); QString a, c, r; KAcceleratorManager::last_manage(a, c, r); if (automatic) { // for now we only show dialogs on F12 checks return; } if (c.isEmpty() && r.isEmpty() && (automatic || a.isEmpty())) { return; } QString s; if (! c.isEmpty()) { s += i18n("

Accelerators changed

"); s += QStringLiteral(""); s += c; s += QStringLiteral("
"); s += i18n("Old Text"); s += QStringLiteral(""); s += i18n("New Text"); s += QStringLiteral("
"); } if (! r.isEmpty()) { s += i18n("

Accelerators removed

"); s += QStringLiteral(""); s += r; s += QStringLiteral("
"); s += i18n("Old Text"); s += QStringLiteral("
"); } if (! a.isEmpty()) { s += i18n("

Accelerators added (just for your info)

"); s += QStringLiteral(""); s += a; s += QStringLiteral("
"); s += i18n("New Text"); s += QStringLiteral("
"); } createDialog(actWin, automatic); drklash_view->setHtml(s); drklash->show(); drklash->raise(); // dlg will be destroyed before returning } #include "kcheckaccelerators.moc" diff --git a/libs/widgetutils/xmlgui/kpartgui.dtd b/libs/widgetutils/xmlgui/kpartgui.dtd index e5ea8c4811..9fe4954500 100644 --- a/libs/widgetutils/xmlgui/kpartgui.dtd +++ b/libs/widgetutils/xmlgui/kpartgui.dtd @@ -1,192 +1,192 @@ - diff --git a/packaging/macos/dmgstyle.sh b/packaging/macos/dmgstyle.sh index 2396ce784d..8c2738ba80 100755 --- a/packaging/macos/dmgstyle.sh +++ b/packaging/macos/dmgstyle.sh @@ -1,186 +1,186 @@ #!/usr/bin/env sh # get krita disk mounted image current properties # creates a dummy folder to aid in setting the layout for # the dmg image file. set_dmgfolder_props() { cp ${DMG_background} "${DMG_STYLE_DIR}/.background" BG_FILENAME=${DMG_background##*/} printf ' tell application "Finder" tell folder "%s" of %s open set current view of container window to icon view set toolbar visible of container window to false set statusbar visible of container window to false set the bounds of container window to {186, 156, 956, 592} set theViewOptions to the icon view options of container window set arrangement of theViewOptions to not arranged set icon size of theViewOptions to 80 set background picture of theViewOptions to file ".background:%s" set position of item "krita.app" of container window to {279, 272} set position of item "Applications" of container window to {597, 272} set position of item "Terms of Use" of container window to {597, 110} update without registering applications close end tell end tell ' "${DMG_STYLE_DIR_OSA}" "${DMG_STYLE_LOCATION}" "${BG_FILENAME}" | osascript get_dmgfolder_props > /dev/null } get_dmgfolder_props() { printf ' tell application "Finder" tell folder "%s" of %s open set v_cv to current view of container window log v_cv set v_tb to toolbar visible of container window log v_tb set v_stat to statusbar visible of container window log v_stat set v_bounds to the bounds of container window log v_bounds set theViewOptions to the icon view options of container window set v_arr to arrangement of theViewOptions log v_arr set v_icons to icon size of theViewOptions log v_icons set v_pos_k to position of item "krita.app" of container window log v_pos_k set v_pos_ap to position of item "Applications" of container window log v_pos_ap set v_pos_term to position of item "Terms of Use" of container window log v_pos_term update without registering applications end tell end tell ' "${DMG_STYLE_DIR_OSA}" "${DMG_STYLE_LOCATION}" | osascript 2>&1 | awk '{printf "%s\n" ,$0}' } # Print Suitable command to paste on osxdeploy.sh print_osascript_cmd() { OLD_IFS=${IFS} IFS=$'\n' dmg_ops=($@) printf 'tell application "Finder" tell disk "%%s" open set current view of container window to %s set toolbar visible of container window to %s set statusbar visible of container window to %s set the bounds of container window to {%s} set theViewOptions to the icon view options of container window set arrangement of theViewOptions to %s set icon size of theViewOptions to %s set background picture of theViewOptions to file ".background:%%s" set position of item "krita.app" of container window to {%s} set position of item "Applications" of container window to {%s} set position of item "Terms of Use" of container window to {%s} update without registering applications delay 1 close end tell end tell ' "${dmg_ops[@]}" IFS=${OLD_IFS} } print_help() { printf \ "dmgstyle.sh is a minitool to aid in the creation of the DMG look for krita out is designed to be copy pasted into osxdeploy.sh in the osasrcipt section - Run once with set to set new background - Edit by moving elements or + J to set net icon size - To get current options for osxdeploy: rerun with no arguments. USAGE: dmgstyle.sh [option] Options: - No option fetchs dummy design window state for paste in osxdeploy + No option fetches dummy design window state for paste in osxdeploy set set default values changing the background to image in " } if test -z ${BUILDROOT}; then echo "ERROR: BUILDROOT env not set!" echo "\t Must point to the root of the buildfiles as stated in 3rdparty Readme" print_help exit fi BUILDROOT=$(cd "$(dirname "${BUILDROOT}")"; pwd -P)/$(basename "${BUILDROOT}") # force full path # if in home we get path minus $HOME # if in Mounted disk we get path minus "/Volumes/diskname" if [[ -n "$(grep /Users/$(whoami) <<< ${BUILDROOT})" ]];then # buildroot is in home build_folder=${BUILDROOT#${HOME}/} # removes home dmg_vol_name="home" else tmp_folder=${BUILDROOT#/Volumes/} build_folder=${tmp_folder#*/} # get minus all printf -v dmg_vol_name 'disk "%s"' ${tmp_folder%%/*} # get volume name fi # Setting global variables DMG_DUMMY_NAME="dmg_style_dummy" DMG_STYLE_DIR="${BUILDROOT}/${DMG_DUMMY_NAME}" DMG_STYLE_DIR_OSA=$(printf "%s/%s" ${build_folder} ${DMG_DUMMY_NAME} | tr / :) DMG_STYLE_LOCATION=${dmg_vol_name} KIS_INSTALL_DIR="${BUILDROOT}/i" if [[ ${1} = "-h" || ${1} = "--help" ]]; then print_help exit fi if [[ ${1} = "set" ]]; then # Create style container folder it if doesn't exist if [[ ! -e ${DMG_STYLE_DIR} ]]; then mkdir "${DMG_STYLE_DIR}" mkdir "${DMG_STYLE_DIR}/Terms of Use" mkdir "${DMG_STYLE_DIR}/.background" ln -s "/Applications" "${DMG_STYLE_DIR}/Applications" ln -s "${KIS_INSTALL_DIR}/bin/krita.app" "${DMG_STYLE_DIR}/krita.app" fi ## check background validity if [[ -f ${2} ]]; then DMG_validBG=0 echo "attempting to check background is valid jpg or png..." BG_FORMAT=$(sips --getProperty format ${2} | awk '{printf $2}') if [[ "png" = ${BG_FORMAT} || "jpeg" = ${BG_FORMAT} ]];then echo "valid image file" DMG_background=$(cd "$(dirname "${2}")"; pwd -P)/$(basename "${2}") DMG_validBG=1 fi fi if [[ ${DMG_validBG} -eq 0 ]]; then echo "No jpg or png given or valid file detected!!" exit else BG_DPI=$(sips --getProperty dpiWidth ${DMG_background} | grep dpi | awk '{print $2}') if [[ $(echo "${BG_DPI} > 150" | bc -l) -eq 1 ]]; then printf "WARNING: image dpi has an effect on apparent size! Check dpi is adequate for screen display if image appears very small Current dpi is: %s\n" ${BG_DPI} fi set_dmgfolder_props fi else if [[ ! -e ${DMG_STYLE_DIR} ]]; then echo "First run must use option [set]!!" exit fi osa_values=$(get_dmgfolder_props) print_osascript_cmd "${osa_values}" fi diff --git a/packaging/macos/osxbuild.sh b/packaging/macos/osxbuild.sh index bcb33201b5..ec39bee74e 100755 --- a/packaging/macos/osxbuild.sh +++ b/packaging/macos/osxbuild.sh @@ -1,536 +1,536 @@ #!/usr/bin/env bash # osxbuild.sh automates building and installing of krita and krita dependencies # for OSX, the script only needs you to set BUILDROOT environment to work # properly. # # Run with no args for a short help about each command. # builddeps: Attempts to build krita dependencies in the necessary order, # intermediate steps for creating symlinks and fixing rpath of some # packages midway is also managed. Order goes from top to bottom, to add # new steps just place them in the proper place. # rebuilddeps: This re-runs all make and make install of dependencies 3rdparty # this was needed as deleting the entire install directory an rerunning build # step for dependencies does not install if they are already built. This step # forces installation. Have not tested it lately so it might not be needed anymore # build: Runs cmake build and make step for krita sources. It always run cmake step, so # it might take a bit longer than a pure on the source tree. The script tries # to set the make flag -jN to a proper N. # install: Runs install step for krita sources. # fixboost: Search for all libraries using boost and sets a proper @rpath for boost as by # default it fails to set a proper @rpath # buildinstall: Runs build, install and fixboost steps.# if test -z $BUILDROOT; then echo "ERROR: BUILDROOT env not set, exiting!" echo "\t Must point to the root of the buildfiles as stated in 3rdparty Readme" exit fi echo "BUILDROOT set to ${BUILDROOT}" export KIS_SRC_DIR=${BUILDROOT}/krita export KIS_TBUILD_DIR=${BUILDROOT}/depbuild export KIS_TDEPINSTALL_DIR=${BUILDROOT}/depinstall export KIS_DOWN_DIR=${BUILDROOT}/down export KIS_BUILD_DIR=${BUILDROOT}/kisbuild export KIS_INSTALL_DIR=${BUILDROOT}/i # flags for OSX environment # Qt only supports from 10.12 up, and https://doc.qt.io/qt-5/macos.html#target-platforms warns against setting it lower export MACOSX_DEPLOYMENT_TARGET=10.12 export QMAKE_MACOSX_DEPLOYMENT_TARGET=10.12 # Build time variables if test -z $(which cmake); then echo "ERROR: cmake not found, exiting!" exit fi export PATH=${KIS_INSTALL_DIR}/bin:$PATH export C_INCLUDE_PATH=${KIS_INSTALL_DIR}/include:/usr/include:${C_INCLUDE_PATH} export CPLUS_INCLUDE_PATH=${KIS_INSTALL_DIR}/include:/usr/include:${CPLUS_INCLUDE_PATH} export LIBRARY_PATH=${KIS_INSTALL_DIR}/lib:/usr/lib:${LIBRARY_PATH} # export CPPFLAGS=-I${KIS_INSTALL_DIR}/include # export LDFLAGS=-L${KIS_INSTALL_DIR}/lib export FRAMEWORK_PATH=${KIS_INSTALL_DIR}/lib/ # export PYTHONHOME=${KIS_INSTALL_DIR} # export PYTHONPATH=${KIS_INSTALL_DIR}/sip:${KIS_INSTALL_DIR}/lib/python3.5/site-packages:${KIS_INSTALL_DIR}/lib/python3.5 # This will make the debug output prettier export KDE_COLOR_DEBUG=1 export QTEST_COLORED=1 export OUPUT_LOG="${BUILDROOT}/osxbuild.log" export ERROR_LOG="${BUILDROOT}/osxbuild-error.log" printf "" > "${OUPUT_LOG}" printf "" > "${ERROR_LOG}" # configure max core for make compile ((MAKE_THREADS=1)) if test ${OSTYPE} == "darwin*"; then ((MAKE_THREADS = $(sysctl -n hw.ncpu) - 1)) fi # Prints stderr and stdout to log files # >(tee) works but breaks sigint log_cmd () { if [[ "${VERBOSE}" ]]; then "$@" 1>> ${OUPUT_LOG} 2>> ${ERROR_LOG} else "$@" 2>> ${ERROR_LOG} | tee -a ${OUPUT_LOG} > /dev/null fi } # Log messages to logfile log () { if [[ "${VERBOSE}" ]]; then printf "%s\n" "${@}" | tee -a ${OUPUT_LOG} else printf "%s\n" "${@}" | tee -a ${OUPUT_LOG} > /dev/null fi } # if previous command gives error # print msg print_if_error() { error_stat="${?}" if [ ${error_stat} -ne 0 ]; then printf "\e[31m%s %s\e[0m\n" "Error:" "${1}" 2>> ${ERROR_LOG} printf "%s\r" "${error_stat}" fi } -# print status messges +# print status messages print_msg() { printf "\e[32m%s\e[0m\n" "${1}" printf "%s\n" "${1}" >> ${OUPUT_LOG} } check_dir_path () { printf "%s" "Checking if ${1} exists and is dir... " if test -d ${1}; then echo -e "OK" elif test -e ${1}; then echo -e "\n\tERROR: file ${1} exists but is not a directory!" >&2 return 1 else echo -e "Creating ${1}" mkdir ${1} fi return 0 } # builds dependencies for the first time cmake_3rdparty () { cd ${KIS_TBUILD_DIR} local build_pkgs=("${@}") # convert to array if [[ ${2} = "1" ]]; then local nofix="true" local build_pkgs=(${build_pkgs[@]:0:1}) fi for package in ${build_pkgs[@]} ; do print_msg "Building ${package}" log_cmd cmake --build . --config RelWithDebInfo --target ${package} if [[ ! $(print_if_error "Failed build ${package}") ]]; then print_msg "Build Success! ${package}" fi # fixes does not depend on failure if [[ ! ${nofix} ]]; then build_3rdparty_fixes ${package} fi done } build_3rdparty_fixes(){ pkg=${1} if [[ "${pkg}" = "ext_qt" && -e "${KIS_INSTALL_DIR}/bin/qmake" ]]; then ln -sf qmake "${KIS_INSTALL_DIR}/bin/qmake-qt5" elif [[ "${pkg}" = "ext_openexr" ]]; then # open exr will fail the first time is called # rpath needs to be fixed an build rerun log "Fixing rpath on openexr file: b44ExpLogTable" log "Fixing rpath on openexr file: dwaLookups" log_cmd install_name_tool -add_rpath ${KIS_INSTALL_DIR}/lib ${KIS_TBUILD_DIR}/ext_openexr/ext_openexr-prefix/src/ext_openexr-build/IlmImf/./b44ExpLogTable log_cmd install_name_tool -add_rpath ${KIS_INSTALL_DIR}/lib ${KIS_TBUILD_DIR}/ext_openexr/ext_openexr-prefix/src/ext_openexr-build/IlmImf/./dwaLookups # we must rerun build! cmake_3rdparty ext_openexr "1" elif [[ "${pkg}" = "ext_fontconfig" ]]; then log "fixing rpath on fc-cache" log_cmd install_name_tool -add_rpath ${KIS_INSTALL_DIR}/lib ${KIS_TBUILD_DIR}/ext_fontconfig/ext_fontconfig-prefix/src/ext_fontconfig-build/fc-cache/.libs/fc-cache # rerun rebuild cmake_3rdparty ext_fontconfig "1" fi } build_3rdparty () { print_msg "building in ${KIS_TBUILD_DIR}" log "$(check_dir_path ${KIS_TBUILD_DIR})" log "$(check_dir_path ${KIS_DOWN_DIR})" log "$(check_dir_path ${KIS_INSTALL_DIR})" cd ${KIS_TBUILD_DIR} log_cmd cmake ${KIS_SRC_DIR}/3rdparty/ \ -DCMAKE_OSX_DEPLOYMENT_TARGET=10.12 \ -DCMAKE_INSTALL_PREFIX=${KIS_INSTALL_DIR} \ -DEXTERNALS_DOWNLOAD_DIR=${KIS_DOWN_DIR} \ -DINSTALL_ROOT=${KIS_INSTALL_DIR} # -DCPPFLAGS=-I${KIS_INSTALL_DIR}/include \ # -DLDFLAGS=-L${KIS_INSTALL_DIR}/lib print_msg "finished 3rdparty build setup" if [[ -n ${1} ]]; then cmake_3rdparty "${@}" exit fi # build 3rdparty tools # The order must not be changed! cmake_3rdparty \ ext_pkgconfig \ ext_gettext \ ext_openssl \ ext_qt \ ext_zlib \ ext_boost \ ext_eigen3 \ ext_exiv2 \ ext_fftw3 \ ext_ilmbase \ ext_jpeg \ ext_lcms2 \ ext_ocio \ ext_openexr cmake_3rdparty \ ext_png \ ext_tiff \ ext_gsl \ ext_vc \ ext_libraw \ ext_giflib \ ext_freetype \ ext_fontconfig \ ext_poppler # Stop if qmake link was not created # this meant qt build fail and further builds will # also fail. log_cmd test -L "${KIS_INSTALL_DIR}/bin/qmake-qt5" if [[ $(print_if_error "qmake link missing!") ]]; then printf " link: ${KIS_INSTALL_DIR}/bin/qmake-qt5 missing! It probably means ext_qt failed!! check, fix and rerun!\n" exit fi # for python cmake_3rdparty \ ext_python \ ext_sip \ ext_pyqt cmake_3rdparty ext_libheif cmake_3rdparty \ ext_extra_cmake_modules \ ext_kconfig \ ext_kwidgetsaddons \ ext_kcompletion \ ext_kcoreaddons \ ext_kguiaddons \ ext_ki18n \ ext_kitemmodels \ ext_kitemviews \ ext_kimageformats \ ext_kwindowsystem \ ext_quazip } # Recall cmake for all 3rd party packages # make is only on target first run # subsequent runs only call make install rebuild_3rdparty () { print_msg "starting rebuild of 3rdparty packages" build_install_ext() { for pkg in ${@:1:${#@}}; do { cd ${KIS_TBUILD_DIR}/${pkg}/${pkg}-prefix/src/${pkg}-stamp } || { cd ${KIS_TBUILD_DIR}/ext_frameworks/${pkg}-prefix/src/${pkg}-stamp } || { cd ${KIS_TBUILD_DIR}/ext_heif/${pkg}-prefix/src/${pkg}-stamp } log "Installing ${pkg} files..." rm ${pkg}-configure ${pkg}-build ${pkg}-install cmake_3rdparty ${pkg} cd ${KIS_TBUILD_DIR} done } # Do not process complete list only pkgs given. if ! test -z ${1}; then build_install_ext ${@} exit fi build_install_ext \ ext_pkgconfig \ ext_iconv \ ext_gettext \ ext_openssl \ ext_qt \ ext_zlib \ ext_boost \ ext_eigen3 \ ext_expat \ ext_exiv2 \ ext_fftw3 \ ext_ilmbase \ ext_jpeg \ ext_patch \ ext_lcms2 \ ext_ocio \ ext_ilmbase \ ext_openexr \ ext_png \ ext_tiff \ ext_gsl \ ext_vc \ ext_libraw \ ext_giflib \ ext_fontconfig \ ext_freetype \ ext_poppler \ ext_python \ ext_sip \ ext_pyqt \ build_install_ext \ ext_yasm \ ext_nasm \ ext_libx265 \ ext_libde265 \ ext_libheif \ # Build kde_frameworks build_install_ext \ ext_extra_cmake_modules \ ext_kconfig \ ext_kwidgetsaddons \ ext_kcompletion \ ext_kcoreaddons \ ext_kguiaddons \ ext_ki18n \ ext_kitemmodels \ ext_kitemviews \ ext_kimageformats \ ext_kwindowsystem \ ext_quazip } #not tested set_krita_dirs() { if [[ -n ${1} ]]; then KIS_BUILD_DIR=${BUILDROOT}/b_${1} KIS_INSTALL_DIR=${BUILDROOT}/i_${1} KIS_SRC_DIR=${BUILDROOT}/src_${1} fi } # build_krita # run cmake krita build_krita () { export DYLD_FRAMEWORK_PATH=${FRAMEWORK_PATH} echo ${KIS_BUILD_DIR} echo ${KIS_INSTALL_DIR} log_cmd check_dir_path ${KIS_BUILD_DIR} cd ${KIS_BUILD_DIR} cmake ${KIS_SRC_DIR} \ -DFOUNDATION_BUILD=ON \ -DBoost_INCLUDE_DIR=${KIS_INSTALL_DIR}/include \ -DCMAKE_INSTALL_PREFIX=${KIS_INSTALL_DIR} \ -DDEFINE_NO_DEPRECATED=1 \ -DBUILD_TESTING=OFF \ -DHIDE_SAFE_ASSERTS=OFF \ -DKDE_INSTALL_BUNDLEDIR=${KIS_INSTALL_DIR}/bin \ -DPYQT_SIP_DIR_OVERRIDE=${KIS_INSTALL_DIR}/share/sip/ \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DCMAKE_OSX_DEPLOYMENT_TARGET=10.12 \ -DPYTHON_INCLUDE_DIR=${KIS_INSTALL_DIR}/lib/Python.framework/Headers # copiling phase make -j${MAKE_THREADS} # compile integrations if test ${OSTYPE} == "darwin*"; then cd ${KIS_BUILD_DIR}/krita/integration/kritaquicklook make -j${MAKE_THREADS} fi } build_krita_tarball () { filename="$(basename ${1})" KIS_CUSTOM_BUILD="${BUILDROOT}/releases/${filename%.tar.gz}" print_msg "Tarball BUILDROOT is ${KIS_CUSTOM_BUILD}" filename_dir=$(dirname "${1}") cd "${filename_dir}" file_abspath="$(pwd)/${1##*/}" mkdir "${KIS_CUSTOM_BUILD}" 2> /dev/null cd "${KIS_CUSTOM_BUILD}" mkdir "src" "build" 2> /dev/null tar -xzf "${file_abspath}" --strip-components=1 --directory "src" if [[ $(print_if_error "Untar ${file_abspath} failed!") ]]; then exit fi KIS_BUILD_DIR="${KIS_CUSTOM_BUILD}/build" KIS_SRC_DIR="${KIS_CUSTOM_BUILD}/src" build_krita print_msg "Build done!" print_msg "to install run osxbuild.sh install ${KIS_BUILD_DIR}" } install_krita () { # custom install provided if [[ -n "${1}" ]]; then KIS_BUILD_DIR="${1}" fi print_msg "Install krita from ${KIS_BUILD_DIR}" log_cmd check_dir_path ${KIS_BUILD_DIR} cd ${KIS_BUILD_DIR} if [[ $(print_if_error "could not cd to ${KIS_BUILD_DIR}") ]]; then exit fi make install # compile integrations if test ${OSTYPE} == "darwin*"; then cd ${KIS_BUILD_DIR}/krita/integration/kritaquicklook make install fi } # Runs all fixes for path and packages. # Historically only fixed boost @rpath fix_boost_rpath () { # helpers to define function only once fixboost_find () { for FILE in "${@}"; do if [[ -n "$(otool -L $FILE | grep boost)" ]]; then log "Fixing -- $FILE" log_cmd install_name_tool -change libboost_system.dylib @rpath/libboost_system.dylib $FILE fi done } batch_fixboost() { xargs -P4 -I FILE bash -c 'fixboost_find "FILE"' } export -f fixboost_find export -f log export -f log_cmd print_msg "Fixing boost in... ${KIS_INSTALL_DIR}" # install_name_tool -add_rpath ${KIS_INSTALL_DIR}/lib $BUILDROOT/$KRITA_INSTALL/bin/krita.app/Contents/MacOS/gmic_krita_qt log_cmd install_name_tool -add_rpath ${KIS_INSTALL_DIR}/lib ${KIS_INSTALL_DIR}/bin/krita.app/Contents/MacOS/krita # echo "Added rpath ${KIS_INSTALL_DIR}/lib to krita bin" # install_name_tool -add_rpath ${BUILDROOT}/deps/lib ${KIS_INSTALL_DIR}/bin/krita.app/Contents/MacOS/krita log_cmd install_name_tool -change libboost_system.dylib @rpath/libboost_system.dylib ${KIS_INSTALL_DIR}/bin/krita.app/Contents/MacOS/krita find -L "${KIS_INSTALL_DIR}" -name '*so' -o -name '*dylib' | batch_fixboost log "Fixing boost done!" } print_usage () { printf "USAGE: osxbuild.sh [pkg|file]\n" printf "BUILDSTEPS:\t\t" printf "\n builddeps \t\t Run cmake step for 3rd party dependencies, optionally takes a [pkg] arg" printf "\n rebuilddeps \t\t Rerun make and make install step for 3rd party deps, optionally takes a [pkg] arg - \t\t\t usefull for cleaning install directory and quickly reinstall all deps." + \t\t\t useful for cleaning install directory and quickly reinstall all deps." printf "\n fixboost \t\t Fixes broken boost \@rpath on OSX" printf "\n build \t\t\t Builds krita" printf "\n buildtarball \t\t\t Builds krita from provided [file] tarball" printf "\n install \t\t Installs krita. Optionally accepts a [build dir] as argument \t\t\t this will install krita from given directory" printf "\n buildinstall \t\t Build and Installs krita, running fixboost after installing" printf "\n" } if [[ ${#} -eq 0 ]]; then echo "ERROR: No option given!" print_usage exit 1 fi if [[ ${1} = "builddeps" ]]; then build_3rdparty "${@:2}" elif [[ ${1} = "rebuilddeps" ]]; then rebuild_3rdparty "${@:2}" elif [[ ${1} = "fixboost" ]]; then if [[ -d ${1} ]]; then KIS_BUILD_DIR="${1}" fi fix_boost_rpath elif [[ ${1} = "build" ]]; then build_krita ${2} elif [[ ${1} = "buildtarball" ]]; then # uncomment line to optionally change # install directory providing a third argument # This is not on by default as build success requires all # deps installed in the given dir beforehand. # KIS_INSTALL_DIR=${3} build_krita_tarball ${2} elif [[ ${1} = "install" ]]; then install_krita ${2} fix_boost_rpath elif [[ ${1} = "buildinstall" ]]; then build_krita ${2} install_krita ${2} fix_boost_rpath ${2} elif [[ ${1} = "test" ]]; then ${KIS_INSTALL_DIR}/bin/krita.app/Contents/MacOS/krita else echo "Option ${1} not supported" print_usage exit 1 fi # after finishig sometimes it complains about missing matching quotes. diff --git a/packaging/macos/osxdeploy.sh b/packaging/macos/osxdeploy.sh index cc11626f94..b181e99230 100755 --- a/packaging/macos/osxdeploy.sh +++ b/packaging/macos/osxdeploy.sh @@ -1,713 +1,713 @@ #!/usr/bin/env bash # Krita tool to create dmg from installed source # Copies all files to a folder to be converted into the final dmg # osxdeploy.sh automates the creation of the release DMG. # default background and style are used if none provided # A short explanation of what it does: # - Copies krita.app contents to kritadmg folder -# - Copies i/share to Contents/Resources excluding unnecesary files +# - Copies i/share to Contents/Resources excluding unnecessary files # - Copies translations, qml and quicklook PlugIns # - Copies i/plugins and i/lib/plugins to Contents/PlugIns # - Runs macdeployqt: macdeployqt is not built by default in ext_qt # build by: # cd ${BUILDROOT}/depbuild/ext_qt/ext_qt-prefix/src/ext_qt/qttools/src # make sub-macdeployqt-all # make sub-macdeployqt-install_subtargets # make install # the script changes dir to installation/bin to run macdeployqt as it can be buggy -# if not runned from the same folder as the binary is on. +# if not run from the same folder as the binary is on. # - Fix rpath from krita bin -# - Find missing libraries from plugins and copy to Framworks or plugins. +# - Find missing libraries from plugins and copy to Frameworks or plugins. # This uses oTool iterative to find all unique libraries, then it searches each # library fond in folder, and if not found attempts to copy contents -# to the appropiate folder, either Frameworks (if frameworks is in namefile, or +# to the appropriate folder, either Frameworks (if frameworks is in namefile, or # library has plugin isnot in path), or plugin if otherwise. # - Builds DMG # Building DMG creates a new dmg with the contents of # mounts the dmg and sets the style for dmg. # unmount # Compress resulting dmg into krita_nightly-.dmg # deletes temporary files. if test -z ${BUILDROOT}; then echo "ERROR: BUILDROOT env not set!" echo "\t Must point to the root of the buildfiles as stated in 3rdparty Readme" echo "exiting..." exit fi -# print status messges +# print status messages print_msg() { printf "\e[32m${1}\e[0m\n" "${@:2}" # printf "%s\n" "${1}" >> ${OUPUT_LOG} } # print error print_error() { printf "\e[31m%s %s\e[0m\n" "Error:" "${1}" } get_script_dir() { script_source="${BASH_SOURCE[0]}" # go to target until finding root. while [ -L "${script_source}" ]; do script_target="$(readlink ${script_source})" if [[ "${script_source}" = /* ]]; then script_source="$script_target" else script_dir="$(dirname "${script_source}")" script_source="${script_dir}/${script_target}" fi done echo "$(dirname ${script_source})" } DMG_title="krita" #if changed krita.temp.dmg must be deleted manually SCRIPT_SOURCE_DIR="$(get_script_dir)" # There is some duplication between build and deploy scripts # a config env file could would be a nice idea. KIS_SRC_DIR=${BUILDROOT}/krita KIS_INSTALL_DIR=${BUILDROOT}/i KIS_BUILD_DIR=${BUILDROOT}/kisbuild # only used for getting git sha number KRITA_DMG=${BUILDROOT}/kritadmg KRITA_DMG_TEMPLATE=${BUILDROOT}/kritadmg-template export PATH=${KIS_INSTALL_DIR}/bin:$PATH # flags for OSX environment # We only support from 10.11 up export MACOSX_DEPLOYMENT_TARGET=10.11 export QMAKE_MACOSX_DEPLOYMENT_TARGET=10.11 print_usage () { printf "USAGE: osxdeploy.sh [-s=] [-notarize-ac=] [-style=] [-bg=] -s \t\t\t Code sign identity for codesign -notarize-ac \t Apple account name for notarization purposes \t\t\t script will attempt to get password from keychain, if fails provide one with \t\t\t the -notarize-pass option: To add a password run \t\t\t security add-generic-password -a \"AC_USERNAME\" -w -s \"KRITA_AC_PASS\" - -notarize-pass \t If given, the Apple acount password. Otherwise an attempt will be macdeployqt_exists + -notarize-pass \t If given, the Apple account password. Otherwise an attempt will be macdeployqt_exists \t\t\t to get the password from keychain using the account given in option. -style \t\t Style file defined from 'dmgstyle.sh' output -bg \t\t Set a background image for dmg folder. \t\t\t osxdeploy needs an input image to attach to the dmg background -\t\t\t image recomended size is at least 950x500 +\t\t\t image recommended size is at least 950x500 " } # Attempt to detach previous mouted DMG if [[ -d "/Volumes/${DMG_title}" ]]; then echo "WARNING: Another Krita DMG is mounted!" echo "Attempting eject…" hdiutil detach "/Volumes/${DMG_title}" if [ $? -ne 0 ]; then exit fi echo "Success!" fi # -- Parse input args for arg in "${@}"; do if [ "${arg}" = -bg=* -a -f "${arg#*=}" ]; then DMG_validBG=0 bg_filename=${arg#*=} echo "attempting to check background is valid jpg or png..." BG_FORMAT=$(sips --getProperty format ${bg_filename} | awk '{printf $2}') if [[ "png" = ${BG_FORMAT} || "jpeg" = ${BG_FORMAT} ]];then echo "valid image file" DMG_background=$(cd "$(dirname "${bg_filename}")"; pwd -P)/$(basename "${bg_filename}") DMG_validBG=1 # check imageDPI BG_DPI=$(sips --getProperty dpiWidth ${DMG_background} | grep dpi | awk '{print $2}') if [[ $(echo "${BG_DPI} > 150" | bc -l) -eq 1 ]]; then printf "WARNING: image dpi has an effect on apparent size! Check dpi is adequate for screen display if image appears very small Current dpi is: %s\n" ${BG_DPI} fi fi fi # If string starts with -sign if [[ ${arg} = -s=* ]]; then CODE_SIGNATURE="${arg#*=}" fi if [[ ${arg} = -notarize-ac=* ]]; then NOTARIZE_ACC="${arg#*=}" fi if [[ ${arg} = -notarize-pass=* ]]; then NOTARIZE_PASS="${arg#*=}" fi if [[ ${arg} = -style=* ]]; then style_filename="${arg#*=}" if [[ -f "${style_filename}" ]]; then DMG_STYLE="${style_filename}" fi fi if [[ ${arg} = "-h" || ${arg} = "--help" ]]; then print_usage exit fi done # -- Checks and messages ### PYTHONAttempt to find python_version local_PY_MAYOR_VERSION=$(python -c "import sys; print(sys.version_info[0])") local_PY_MINOR_VERSION=$(python -c "import sys; print(sys.version_info[1])") PY_VERSION="${local_PY_MAYOR_VERSION}.${local_PY_MINOR_VERSION}" print_msg "Detected Python %s" "${PY_VERSION}" ### Code Signature & NOTARIZATION NOTARIZE="false" if [[ -z "${CODE_SIGNATURE}" ]]; then echo "WARNING: No code signature provided, Code will not be signed" else print_msg "Code will be signed with %s" "${CODE_SIGNATURE}" ### NOTARIZATION if [[ -n "${NOTARIZE_ACC}" ]]; then security find-generic-password -s "KRITA_AC_PASS" > /dev/null 2>&1 if [[ ${?} -eq 0 || -n "${NOTARIZE_PASS}" ]]; then NOTARIZE="true" else echo "No password given for notarization or KRITA_AC_PASS missig in keychain" fi fi fi if [[ ${NOTARIZE} = "true" ]]; then print_msg "Notarization checks complete, This build will be notarized" else echo "WARNING: Account information missing, Notarization will not be performed" fi ### STYLE for DMG if [[ ! ${DMG_STYLE} ]]; then DMG_STYLE="${SCRIPT_SOURCE_DIR}/default.style" fi print_msg "Using style from: %s" "${DMG_STYLE}" ### Background for DMG if [[ ${DMG_validBG} -eq 0 ]]; then echo "No jpg or png valid file detected!!" echo "Using default style" DMG_background="${SCRIPT_SOURCE_DIR}/krita_dmgBG.jpg" fi # Helper functions countArgs () { echo "${#}" } stringContains () { echo "$(grep "${2}" <<< "${1}")" } waiting_fixed() { local message="${1}" local waitTime=${2} for i in $(seq ${waitTime}); do sleep 1 printf -v dots '%*s' ${i} printf -v spaces '%*s' $((${waitTime} - $i)) printf "\r%s [%s%s]" "${message}" "${dots// /.}" "${spaces}" done printf "\n" } add_lib_to_list() { local llist=${2} if test -z "$(grep ${1##*/} <<< ${llist})" ; then local llist="${llist} ${1##*/} " fi echo "${llist}" } # Find all @rpath and Absolute to buildroot path libs # Add to libs_used # converts absolute buildroot path to @rpath find_needed_libs () { - # echo "Analizing libraries with oTool..." >&2 + # echo "Analyzing libraries with oTool..." >&2 local libs_used="" # input lib_lists founded for libFile in ${@}; do if test -z "$(file ${libFile} | grep 'Mach-O')" ; then # echo "skipping ${libFile}" >&2 continue fi oToolResult=$(otool -L ${libFile} | awk '{print $1}') resultArray=(${oToolResult}) # convert to array for lib in ${resultArray[@]:1}; do if test "${lib:0:1}" = "@"; then local libs_used=$(add_lib_to_list "${lib}" "${libs_used}") fi if [[ "${lib:0:${#BUILDROOT}}" = "${BUILDROOT}" ]]; then printf "Fixing %s: %s\n" "${libFile#${KRITA_DMG}/}" "${lib##*/}" >&2 if [[ "${lib##*/}" = "${libFile##*/}" ]]; then install_name_tool -id ${lib##*/} "${libFile}" else install_name_tool -change ${lib} "@rpath/${lib##*${BUILDROOT}/i/lib/}" "${libFile}" local libs_used=$(add_lib_to_list "${lib}" "${libs_used}") fi fi done done echo "${libs_used}" # return updated list } find_missing_libs (){ # echo "Searching for missing libs on deployment folders…" >&2 local libs_missing="" for lib in ${@}; do if test -z "$(find ${KRITA_DMG}/krita.app/Contents/ -name ${lib})"; then # echo "Adding ${lib} to missing libraries." >&2 libs_missing="${libs_missing} ${lib}" fi done echo "${libs_missing}" } copy_missing_libs () { for lib in ${@}; do result=$(find -L "${BUILDROOT}/i" -name "${lib}") if test $(countArgs ${result}) -eq 1; then if [ "$(stringContains "${result}" "plugin")" ]; then cp -pv ${result} ${KRITA_DMG}/krita.app/Contents/PlugIns/ krita_findmissinglibs "${KRITA_DMG}/krita.app/Contents/PlugIns/${result##*/}" else cp -pv ${result} ${KRITA_DMG}/krita.app/Contents/Frameworks/ krita_findmissinglibs "${KRITA_DMG}/krita.app/Contents/Frameworks/${result##*/}" fi else echo "${lib} might be a missing framework" if [ "$(stringContains "${result}" "framework")" ]; then echo "copying framework ${BUILDROOT}/i/lib/${lib}.framework to dmg" # rsync only included ${lib} Resources Versions rsync -priul ${BUILDROOT}/i/lib/${lib}.framework/${lib} ${KRITA_DMG}/krita.app/Contents/Frameworks/${lib}.framework/ rsync -priul ${BUILDROOT}/i/lib/${lib}.framework/Resources ${KRITA_DMG}/krita.app/Contents/Frameworks/${lib}.framework/ rsync -priul ${BUILDROOT}/i/lib/${lib}.framework/Versions ${KRITA_DMG}/krita.app/Contents/Frameworks/${lib}.framework/ krita_findmissinglibs "$(find "${KRITA_DMG}/krita.app/Contents/Frameworks/${lib}.framework/" -type f -perm 755)" fi fi done } krita_findmissinglibs() { neededLibs=$(find_needed_libs "${@}") missingLibs=$(find_missing_libs ${neededLibs}) if test $(countArgs ${missingLibs}) -gt 0; then printf "Found missing libs: %s\n" "${missingLibs}" copy_missing_libs ${missingLibs} fi } strip_python_dmginstall() { # reduce size of framework python # Removes tests, installers, pyenv, distutils - echo "Removing unnecesary files from Python.Framework to be packaged..." + echo "Removing unnecessary files from Python.Framework to be packaged..." PythonFrameworkBase="${KRITA_DMG}/krita.app/Contents/Frameworks/Python.framework" cd ${PythonFrameworkBase} find . -name "test*" -type d | xargs rm -rf find "${PythonFrameworkBase}/Versions/${PY_VERSION}/bin" -not -name "python*" \( -type f -or -type l \) | xargs rm -f cd "${PythonFrameworkBase}/Versions/${PY_VERSION}/lib/python${PY_VERSION}" rm -rf distutils tkinter ensurepip venv lib2to3 idlelib } # Some libraries require r_path to be removed # we must not apply delete rpath globally delete_install_rpath() { xargs -P4 -I FILE install_name_tool -delete_rpath "${BUILDROOT}/i/lib" FILE 2> "${BUILDROOT}/deploy_error.log" } fix_python_framework() { # Fix python.framework rpath and slims down installation PythonFrameworkBase="${KRITA_DMG}/krita.app/Contents/Frameworks/Python.framework" # Fix main library pythonLib="${PythonFrameworkBase}/Python" install_name_tool -id "${pythonLib##*/}" "${pythonLib}" install_name_tool -add_rpath @loader_path/../../../ "${pythonLib}" 2> /dev/null install_name_tool -change @loader_path/../../../../libintl.9.dylib @loader_path/../../../libintl.9.dylib "${pythonLib}" # Fix all executables install_name_tool -add_rpath @executable_path/../../../../../../../ "${PythonFrameworkBase}/Versions/Current/Resources/Python.app/Contents/MacOS/Python" install_name_tool -change "${KIS_INSTALL_DIR}/lib/Python.framework/Versions/${PY_VERSION}/Python" @executable_path/../../../../../../Python "${PythonFrameworkBase}/Versions/Current/Resources/Python.app/Contents/MacOS/Python" install_name_tool -add_rpath @executable_path/../../../../ "${PythonFrameworkBase}/Versions/Current/bin/python${PY_VERSION}" install_name_tool -add_rpath @executable_path/../../../../ "${PythonFrameworkBase}/Versions/Current/bin/python${PY_VERSION}m" # Fix rpaths from Python.Framework find ${PythonFrameworkBase} -type f -perm 755 | delete_install_rpath find "${PythonFrameworkBase}/Versions/Current/site-packages/PyQt5" -type f -name "*.so" | delete_install_rpath } # Checks for macdeployqt # If not present attempts to install # If it fails shows an informatve message # (For now, macdeployqt is fundamental to deploy) macdeployqt_exists() { printf "Checking for macdeployqt... " if [[ ! -e "${KIS_INSTALL_DIR}/bin/macdeployqt" ]]; then printf "Not Found!\n" printf "Attempting to install macdeployqt\n" cd ${BUILDROOT}/depbuild/ext_qt/ext_qt-prefix/src/ext_qt/qttools/src make sub-macdeployqt-all make sub-macdeployqt-install_subtargets make install if [[ ! -e "${KIS_INSTALL_DIR}/bin/macdeployqt" ]]; then printf " ERROR: Failed to install macdeployqt! Compile and install from qt source directory Source code to build could be located in qttools/src in qt source dir: ${BUILDROOT}/depbuild/ext_qt/ext_qt-prefix/src/ext_qt/qttools/src From the source dir, build and install: make sub-macdeployqt-all make sub-macdeployqt-install_subtargets make install " printf "\nexiting...\n" exit else echo "Done!" fi else echo "Found!" fi } krita_deploy () { # check for macdeployqt macdeployqt_exists cd ${BUILDROOT} # Update files in krita.app echo "Deleting previous kritadmg run..." rm -rf ./krita.dmg ${KRITA_DMG} # Copy new builtFiles echo "Preparing ${KRITA_DMG} for deployment..." echo "Copying krita.app..." mkdir "${KRITA_DMG}" rsync -prul ${KIS_INSTALL_DIR}/bin/krita.app ${KRITA_DMG} mkdir -p ${KRITA_DMG}/krita.app/Contents/PlugIns mkdir -p ${KRITA_DMG}/krita.app/Contents/Frameworks echo "Copying share..." # Deletes old copies of translation and qml to be recreated cd ${KIS_INSTALL_DIR}/share/ rsync -prul --delete ./ \ --exclude krita_SRCS.icns \ --exclude aclocal \ --exclude doc \ --exclude ECM \ --exclude eigen3 \ --exclude emacs \ --exclude gettext \ --exclude gettext-0.19.8 \ --exclude info \ --exclude kf5 \ --exclude kservices5 \ --exclude man \ --exclude ocio \ --exclude pkgconfig \ --exclude mime \ --exclude translations \ --exclude qml \ ${KRITA_DMG}/krita.app/Contents/Resources cd ${BUILDROOT} echo "Copying translations..." rsync -prul ${KIS_INSTALL_DIR}/translations/ \ ${KRITA_DMG}/krita.app/Contents/Resources/translations echo "Copying kritaquicklook..." mkdir -p ${KRITA_DMG}/krita.app/Contents/Library/QuickLook rsync -prul ${KIS_INSTALL_DIR}/plugins/kritaquicklook.qlgenerator ${KRITA_DMG}/krita.app/Contents/Library/QuickLook cd ${KRITA_DMG}/krita.app/Contents ln -shF Resources share echo "Copying qml..." rsync -prul ${KIS_INSTALL_DIR}/qml Resources/qml echo "Copying plugins..." # exclude kritaquicklook.qlgenerator/ cd ${KIS_INSTALL_DIR}/plugins/ rsync -prul --delete --delete-excluded ./ \ --exclude kritaquicklook.qlgenerator \ ${KRITA_DMG}/krita.app/Contents/PlugIns cd ${BUILDROOT} rsync -prul ${KIS_INSTALL_DIR}/lib/kritaplugins/ ${KRITA_DMG}/krita.app/Contents/PlugIns # rsync -prul {KIS_INSTALL_DIR}/lib/libkrita* Frameworks/ # To avoid errors macdeployqt must be run from bin location # ext_qt will not build macdeployqt by default so it must be build manually # cd ${BUILDROOT}/depbuild/ext_qt/ext_qt-prefix/src/ext_qt/qttools/src # make sub-macdeployqt-all # make sub-macdeployqt-install_subtargets # make install echo "Running macdeployqt..." cd ${KIS_INSTALL_DIR}/bin ./macdeployqt ${KRITA_DMG}/krita.app \ -verbose=0 \ -executable=${KRITA_DMG}/krita.app/Contents/MacOS/krita \ -libpath=${KIS_INSTALL_DIR}/lib \ -qmldir=${KIS_INSTALL_DIR}/qml \ # -extra-plugins=${KIS_INSTALL_DIR}/lib/kritaplugins \ # -extra-plugins=${KIS_INSTALL_DIR}/lib/plugins \ # -extra-plugins=${KIS_INSTALL_DIR}/plugins cd ${BUILDROOT} echo "macdeployqt done!" echo "Copying python..." # Copy this framework last! # It is best that macdeployqt does not modify Python.framework # folders with period in name are treated as Frameworks for codesign rsync -prul ${KIS_INSTALL_DIR}/lib/Python.framework ${KRITA_DMG}/krita.app/Contents/Frameworks/ rsync -prul ${KIS_INSTALL_DIR}/lib/krita-python-libs ${KRITA_DMG}/krita.app/Contents/Frameworks/ # change perms on Python to allow header change chmod +w ${KRITA_DMG}/krita.app/Contents/Frameworks/Python.framework/Python fix_python_framework strip_python_dmginstall # fix python pyc # precompile all pyc so the dont alter signature echo "Precompiling all python files..." cd ${KRITA_DMG}/krita.app ${KIS_INSTALL_DIR}/bin/python -m compileall . &> /dev/null install_name_tool -delete_rpath @loader_path/../../../../lib ${KRITA_DMG}/krita.app/Contents/MacOS/krita rm -rf ${KRITA_DMG}/krita.app/Contents/PlugIns/kf5/org.kde.kwindowsystem.platforms # repair krita for plugins printf "Searching for missing libraries\n" krita_findmissinglibs $(find ${KRITA_DMG}/krita.app/Contents -type f -perm 755 -or -name "*.dylib" -or -name "*.so") printf "removing absolute or broken linksys, if any\n" find "${KRITA_DMG}/krita.app/Contents" -type l \( -lname "/*" -or -not -exec test -e {} \; \) -print | xargs rm echo "Done!" } # helper to define function only once batch_codesign() { xargs -P4 -I FILE codesign --options runtime --timestamp -f -s "${CODE_SIGNATURE}" FILE } # Code sign must be done as recommended by apple "sign code inside out in individual stages" signBundle() { cd ${KRITA_DMG} # sign Frameworks and libs cd ${KRITA_DMG}/krita.app/Contents/Frameworks - # remove debug version as both versions cant be signed. + # remove debug version as both versions can't be signed. rm ${KRITA_DMG}/krita.app/Contents/Frameworks/QtScript.framework/Versions/Current/QtScript_debug find . -type f -perm 755 -or -name "*.dylib" -or -name "*.so" | batch_codesign find . -type d -name "*.framework" | xargs printf "%s/Versions/Current\n" | batch_codesign # Sign all other files in Framework (needed) # there are many files in python do we need to sign them all? find krita-python-libs -type f | batch_codesign # find python -type f | batch_codesign # Sing only libraries and plugins cd ${KRITA_DMG}/krita.app/Contents/PlugIns find . -type f | batch_codesign cd ${KRITA_DMG}/krita.app/Contents/Library/QuickLook printf "kritaquicklook.qlgenerator" | batch_codesign # It is recommended to sign every Resource file cd ${KRITA_DMG}/krita.app/Contents/Resources find . -type f | batch_codesign #Finally sign krita and krita.app printf "${KRITA_DMG}/krita.app/Contents/MacOS/krita" | batch_codesign printf "${KRITA_DMG}/krita.app" | batch_codesign } # Notarize build on macOS servers # based on https://github.com/Beep6581/RawTherapee/blob/6fa533c40b34dec527f1176d47cc6c683422a73f/tools/osx/macosx_bundle.sh#L225-L250 notarize_build() { local NOT_SRC_DIR=${1} local NOT_SRC_FILE=${2} if [[ ${NOTARIZE} = "true" ]]; then printf "performing notarization of %s\n" "${2}" cd "${NOT_SRC_DIR}" if [[ -z "${NOTARIZE_PASS}" ]]; then NOTARIZE_PASS="@keychain:KRITA_AC_PASS" fi ditto -c -k --sequesterRsrc --keepParent "${NOT_SRC_FILE}" "${BUILDROOT}/tmp_notarize/${NOT_SRC_FILE}.zip" # echo "xcrun altool --notarize-app --primary-bundle-id \"org.krita\" --username \"${NOTARIZE_ACC}\" --password \"${NOTARIZE_PASS}\" --file \"${BUILDROOT}/tmp_notarize/${NOT_SRC_FILE}.zip\"" local altoolResponse="$(xcrun altool --notarize-app --primary-bundle-id "org.krita" --username "${NOTARIZE_ACC}" --password "${NOTARIZE_PASS}" --file "${BUILDROOT}/tmp_notarize/${NOT_SRC_FILE}.zip" 2>&1)" if [[ -n "$(grep 'Error' <<< ${altoolResponse})" ]]; then printf "ERROR: xcrun altool exited with the following error! \n\n%s\n\n" "${altoolResponse}" - printf "This could mean there is an error in AppleID autentication!\n" + printf "This could mean there is an error in AppleID authentication!\n" printf "aborting notarization\n" NOTARIZE="false" return else printf "Response:\n\n%s\n\n" "${altoolResponse}" fi local uuid="$(grep 'RequestUUID' <<< ${altoolResponse} | awk '{ print $3 }')" echo "RequestUUID = ${uuid}" # Display identifier string waiting_fixed "Waiting to retrieve notarize status" 15 while true ; do fullstatus=$(xcrun altool --notarization-info "${uuid}" --username "${NOTARIZE_ACC}" --password "${NOTARIZE_PASS}" 2>&1) # get the status notarize_status=`echo "${fullstatus}" | grep 'Status\:' | awk '{ print $2 }'` echo "${fullstatus}" if [[ "${notarize_status}" = "success" ]]; then xcrun stapler staple "${NOT_SRC_FILE}" #staple the ticket xcrun stapler validate -v "${NOT_SRC_FILE}" print_msg "Notarization success!" break elif [[ "${notarize_status}" = "in" ]]; then waiting_fixed "Notarization still in progress, sleeping for 15 seconds and trying again" 15 else echo "Notarization failed! full status below" echo "${fullstatus}" exit 1 fi done fi } createDMG () { printf "Creating of dmg with contents of %s...\n" "${KRITA_DMG}" cd ${BUILDROOT} DMG_size=700 ## Build dmg from folder # create dmg on local system # usage of -fsargs minimze gaps at front of filesystem (reduce size) hdiutil create -srcfolder "${KRITA_DMG}" -volname "${DMG_title}" -fs HFS+ \ -fsargs "-c c=64,a=16,e=16" -format UDRW -size ${DMG_size}m krita.temp.dmg # Next line is only useful if we have a dmg as a template! # previous hdiutil must be uncommented # cp krita-template.dmg krita.dmg device=$(hdiutil attach -readwrite -noverify -noautoopen "krita.temp.dmg" | egrep '^/dev/' | sed 1q | awk '{print $1}') # rsync -priul --delete ${KRITA_DMG}/krita.app "/Volumes/${DMG_title}" # Set style for dmg if [[ ! -d "/Volumes/${DMG_title}/.background" ]]; then mkdir "/Volumes/${DMG_title}/.background" fi cp -v ${DMG_background} "/Volumes/${DMG_title}/.background/" mkdir "/Volumes/${DMG_title}/Terms of Use" cp -v "${KIS_SRC_DIR}/packaging/macos/Terms_of_use.rtf" "/Volumes/${DMG_title}/Terms of Use/" ln -s "/Applications" "/Volumes/${DMG_title}/Applications" ## Apple script to set style style="$(<"${DMG_STYLE}")" printf "${style}" "${DMG_title}" "${DMG_background##*/}" | osascript #Set Icon for DMG cp -v "${SCRIPT_SOURCE_DIR}/KritaIcon.icns" "/Volumes/${DMG_title}/.VolumeIcon.icns" SetFile -a C "/Volumes/${DMG_title}" chmod -Rf go-w "/Volumes/${DMG_title}" - # ensure all writting operations to dmg are over + # ensure all writing operations to dmg are over sync hdiutil detach $device hdiutil convert "krita.temp.dmg" -format UDZO -imagekey -zlib-level=9 -o krita-out.dmg # Add git version number GIT_SHA=$(grep "#define KRITA_GIT_SHA1_STRING" ${KIS_BUILD_DIR}/libs/version/kritagitversion.h | awk '{gsub(/"/, "", $3); printf $3}') mv krita-out.dmg krita-nightly_${GIT_SHA}.dmg echo "moved krita-out.dmg to krita-nightly_${GIT_SHA}.dmg" rm krita.temp.dmg if [[ -n "${CODE_SIGNATURE}" ]]; then printf "krita-nightly_${GIT_SHA}.dmg" | batch_codesign fi notarize_build ${BUILDROOT} "krita-nightly_${GIT_SHA}.dmg" echo "dmg done!" } ####################### # Program starts!! ######################## -# Run deploy command, instalation is assumed to exist in BUILDROOT/i +# Run deploy command, installation is assumed to exist in BUILDROOT/i krita_deploy # Code sign krita.app if signature given if [[ -n "${CODE_SIGNATURE}" ]]; then signBundle fi notarize_build ${KRITA_DMG} krita.app # Create DMG from files inside ${KRITA_DMG} folder createDMG if [[ "${NOTARIZE}" = "false" ]]; then macosVersion="$(sw_vers | grep ProductVersion | awk ' BEGIN { FS = "[ .\t]" } { print $3} ')" if (( ${macosVersion} == 15 )); then print_error "Build not notarized! Needed for macOS versions above 10.14" fi fi diff --git a/packaging/windows/installer/installer_krita.nsi b/packaging/windows/installer/installer_krita.nsi index 41140f640e..20819075e6 100644 --- a/packaging/windows/installer/installer_krita.nsi +++ b/packaging/windows/installer/installer_krita.nsi @@ -1,662 +1,662 @@ !ifndef KRITA_INSTALLER_32 & KRITA_INSTALLER_64 !error "Either one of KRITA_INSTALLER_32 or KRITA_INSTALLER_64 must be defined." !endif !ifdef KRITA_INSTALLER_32 & KRITA_INSTALLER_64 !error "Only one of KRITA_INSTALLER_32 or KRITA_INSTALLER_64 should be defined." !endif !ifndef KRITA_PACKAGE_ROOT !error "KRITA_PACKAGE_ROOT should be defined and point to the root of the package files." !endif !ifdef KRITA_INSTALLER_64 !define KRITA_INSTALLER_BITNESS 64 !else !define KRITA_INSTALLER_BITNESS 32 !endif Unicode true ManifestDPIAware true # Krita constants (can be overridden in command line params) !define /ifndef KRITA_VERSION "0.0.0.0" !define /ifndef KRITA_VERSION_DISPLAY "test-version" #!define /ifndef KRITA_VERSION_GIT "" !define /ifndef KRITA_INSTALLER_OUTPUT_DIR "" !ifdef KRITA_INSTALLER_64 !define /ifndef KRITA_INSTALLER_OUTPUT_NAME "krita_x64_setup.exe" !else !define /ifndef KRITA_INSTALLER_OUTPUT_NAME "krita_x86_setup.exe" !endif # Krita constants (fixed) !if "${KRITA_INSTALLER_OUTPUT_DIR}" == "" !define KRITA_INSTALLER_OUTPUT "${KRITA_INSTALLER_OUTPUT_NAME}" !else !define KRITA_INSTALLER_OUTPUT "${KRITA_INSTALLER_OUTPUT_DIR}\${KRITA_INSTALLER_OUTPUT_NAME}" !endif !define KRTIA_PUBLISHER "Krita Foundation" !ifdef KRITA_INSTALLER_64 !define KRITA_PRODUCTNAME "Krita (x64)" !define KRITA_UNINSTALL_REGKEY "Krita_x64" !else !define KRITA_PRODUCTNAME "Krita (x86)" !define KRITA_UNINSTALL_REGKEY "Krita_x86" !endif VIProductVersion "${KRITA_VERSION}" VIAddVersionKey "CompanyName" "${KRTIA_PUBLISHER}" VIAddVersionKey "FileDescription" "${KRITA_PRODUCTNAME} ${KRITA_VERSION_DISPLAY} Setup" VIAddVersionKey "FileVersion" "${KRITA_VERSION}" VIAddVersionKey "InternalName" "${KRITA_INSTALLER_OUTPUT_NAME}" VIAddVersionKey "LegalCopyright" "${KRTIA_PUBLISHER}" VIAddVersionKey "OriginalFileName" "${KRITA_INSTALLER_OUTPUT_NAME}" VIAddVersionKey "ProductName" "${KRITA_PRODUCTNAME} ${KRITA_VERSION_DISPLAY} Setup" VIAddVersionKey "ProductVersion" "${KRITA_VERSION}" BrandingText "[NSIS ${NSIS_VERSION}] ${KRITA_PRODUCTNAME} ${KRITA_VERSION}" Name "${KRITA_PRODUCTNAME} ${KRITA_VERSION_DISPLAY}" OutFile ${KRITA_INSTALLER_OUTPUT} !ifdef KRITA_INSTALLER_64 InstallDir "$PROGRAMFILES64\Krita (x64)" !else InstallDir "$PROGRAMFILES32\Krita (x86)" !endif XPstyle on ShowInstDetails show ShowUninstDetails show Var KritaStartMenuFolder Var CreateDesktopIcon !include MUI2.nsh !define MUI_FINISHPAGE_NOAUTOCLOSE # Installer Pages !insertmacro MUI_PAGE_WELCOME !define MUI_LICENSEPAGE_CHECKBOX !insertmacro MUI_PAGE_LICENSE "license_gpl-3.0.rtf" !insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_COMPONENTS !define MUI_PAGE_CUSTOMFUNCTION_PRE func_ShellExLicensePage_Init !define MUI_PAGE_HEADER_TEXT "License Agreement (Krita Shell Extension)" !insertmacro MUI_PAGE_LICENSE "license.rtf" !define MUI_STARTMENUPAGE_DEFAULTFOLDER "Krita" !define MUI_STARTMENUPAGE_REGISTRY_ROOT HKLM !define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\Krita" !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "StartMenuFolder" !define MUI_STARTMENUPAGE_NODISABLE !insertmacro MUI_PAGE_STARTMENU Krita $KritaStartMenuFolder Page Custom func_DesktopShortcutPage_Init Page Custom func_BeforeInstallPage_Init !insertmacro MUI_PAGE_INSTFILES !insertmacro MUI_PAGE_FINISH # Uninstaller Pages !insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_INSTFILES !insertmacro MUI_LANGUAGE "English" !include Sections.nsh !include LogicLib.nsh !include x64.nsh !include WinVer.nsh !include WordFunc.nsh !define KRITA_SHELLEX_DIR "$INSTDIR\shellex" !include "include\FileExists2.nsh" !include "include\IsFileInUse.nsh" !include "krita_versions_detect.nsh" !include "krita_shell_integration.nsh" Var KritaMsiProductX86 Var KritaMsiProductX64 Var KritaNsisVersion Var KritaNsisBitness Var KritaNsisInstallLocation Var PrevShellExInstallLocation Var PrevShellExStandalone Var UninstallShellExStandalone Section "-Remove_shellex" SEC_remove_shellex ${If} $PrevShellExInstallLocation != "" ${AndIf} $PrevShellExStandalone == 1 ${AndIf} $KritaNsisVersion == "" ${AndIf} ${FileExists} "$PrevShellExInstallLocation\uninstall.exe" push $R0 DetailPrint "Removing Krita Shell Integration..." SetDetailsPrint listonly ExecWait "$PrevShellExInstallLocation\uninstall.exe /S _?=$PrevShellExInstallLocation" $R0 ${If} $R0 != 0 ${IfNot} ${Silent} MessageBox MB_OK|MB_ICONSTOP "Failed to remove Krita Shell Integration." ${EndIf} SetDetailsPrint both DetailPrint "Failed to remove Krita Shell Integration." Abort ${EndIf} Delete "$PrevShellExInstallLocation\uninstall.exe" RMDir /REBOOTOK "$PrevShellExInstallLocation" SetRebootFlag false SetDetailsPrint lastused DetailPrint "Krita Shell Integration removed." pop $R0 ${EndIf} SectionEnd Section "Remove Old Version" SEC_remove_old_version ${If} $KritaNsisInstallLocation != "" ${AndIf} ${FileExists} "$KritaNsisInstallLocation\uninstall.exe" push $R0 DetailPrint "Removing previous version..." SetDetailsPrint listonly ExecWait "$KritaNsisInstallLocation\uninstall.exe /S _?=$KritaNsisInstallLocation" $R0 ${If} $R0 != 0 ${IfNot} ${Silent} MessageBox MB_OK|MB_ICONSTOP "Failed to remove previous version of Krita." ${EndIf} SetDetailsPrint both DetailPrint "Failed to remove previous version of Krita." Abort ${EndIf} Delete "$KritaNsisInstallLocation\uninstall.exe" RMDir /REBOOTOK "$KritaNsisInstallLocation" SetRebootFlag false SetDetailsPrint lastused DetailPrint "Previous version removed." pop $R0 ${EndIf} SectionEnd Section "-Thing" SetOutPath $INSTDIR WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${KRITA_UNINSTALL_REGKEY}" \ "DisplayName" "${KRITA_PRODUCTNAME} ${KRITA_VERSION_DISPLAY}" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${KRITA_UNINSTALL_REGKEY}" \ "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" WriteUninstaller $INSTDIR\uninstall.exe WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${KRITA_UNINSTALL_REGKEY}" \ "DisplayVersion" "${KRITA_VERSION}" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${KRITA_UNINSTALL_REGKEY}" \ "DisplayIcon" "$\"$INSTDIR\shellex\krita.ico$\",0" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${KRITA_UNINSTALL_REGKEY}" \ "URLInfoAbout" "https://krita.org/" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${KRITA_UNINSTALL_REGKEY}" \ "InstallLocation" "$INSTDIR" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${KRITA_UNINSTALL_REGKEY}" \ "Publisher" "${KRTIA_PUBLISHER}" #WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${KRITA_UNINSTALL_REGKEY}" \ # "EstimatedSize" 250000 WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${KRITA_UNINSTALL_REGKEY}" \ "NoModify" 1 WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${KRITA_UNINSTALL_REGKEY}" \ "NoRepair" 1 # Registry entries for version recognition # InstallLocation: # Where krita is installed WriteRegStr HKLM "Software\Krita" \ "InstallLocation" "$INSTDIR" # Version: # Version of Krita WriteRegStr HKLM "Software\Krita" \ "Version" "${KRITA_VERSION}" # x64: # Set to 1 for 64-bit Krita, can be missing for 32-bit Krita !ifdef KRITA_INSTALLER_64 WriteRegDWORD HKLM "Software\Krita" \ "x64" 1 !else DeleteRegValue HKLM "Software\Krita" "x64" !endif # StartMenuFolder: # Start Menu Folder # Handled by Modern UI 2.0 MUI_PAGE_STARTMENU SectionEnd Section "${KRITA_PRODUCTNAME}" SEC_product_main # TODO: Maybe switch to explicit file list? File /r /x ffmpeg.exe /x ffmpeg_README.txt /x ffmpeg_LICENSE.txt ${KRITA_PACKAGE_ROOT}\bin File /r ${KRITA_PACKAGE_ROOT}\lib File /r ${KRITA_PACKAGE_ROOT}\share File /r ${KRITA_PACKAGE_ROOT}\python SectionEnd Section "-Main_associate" CreateDirectory ${KRITA_SHELLEX_DIR} ${Krita_RegisterFileAssociation} "$INSTDIR\bin\krita.exe" SectionEnd Section "-Main_Shortcuts" # Placing this after Krita_RegisterFileAssociation to get the icon !insertmacro MUI_STARTMENU_WRITE_BEGIN Krita CreateDirectory "$SMPROGRAMS\$KritaStartMenuFolder" CreateShortcut "$SMPROGRAMS\$KritaStartMenuFolder\${KRITA_PRODUCTNAME}.lnk" "$INSTDIR\bin\krita.exe" "" "$INSTDIR\shellex\krita.ico" 0 CreateDirectory "$SMPROGRAMS\$KritaStartMenuFolder\Tools" CreateShortcut "$SMPROGRAMS\$KritaStartMenuFolder\Tools\Uninstall ${KRITA_PRODUCTNAME}.lnk" "$INSTDIR\Uninstall.exe" !insertmacro MUI_STARTMENU_WRITE_END ${If} $CreateDesktopIcon == 1 # For the desktop icon, keep the name short and omit version info CreateShortcut "$DESKTOP\Krita.lnk" "$INSTDIR\bin\krita.exe" "" "$INSTDIR\shellex\krita.ico" 0 ${EndIf} SectionEnd Section "Shell Integration" SEC_shellex ${If} ${RunningX64} ${Krita_RegisterComComonents} 64 ${EndIf} ${Krita_RegisterComComonents} 32 ${Krita_RegisterShellExtension} # ShellExtension\InstallLocation: # Where the shell extension is installed # If installed by Krita installer, this must point to shellex sub-dir WriteRegStr HKLM "Software\Krita\ShellExtension" \ "InstallLocation" "$INSTDIR\shellex" # ShellExtension\Version: # Version of the shell extension WriteRegStr HKLM "Software\Krita\ShellExtension" \ "Version" "${KRITASHELLEX_VERSION}" # ShellExtension\Standalone: # 0 = Installed by Krita installer # 1 = Standalone installer WriteRegDWORD HKLM "Software\Krita\ShellExtension" \ "Standalone" 0 # ShellExtension\KritaExePath: # Path to krita.exe as specified by user or by Krita installer # Empty if not specified WriteRegStr HKLM "Software\Krita\ShellExtension" \ "KritaExePath" "$INSTDIR\bin\krita.exe" SectionEnd !ifdef HAS_FFMPEG Section "Bundled FFmpeg" SEC_ffmpeg File /oname=bin\ffmpeg.exe ${KRITA_PACKAGE_ROOT}\bin\ffmpeg.exe File /oname=bin\ffmpeg_LICENSE.txt ${KRITA_PACKAGE_ROOT}\bin\ffmpeg_LICENSE.txt File /oname=bin\ffmpeg_README.txt ${KRITA_PACKAGE_ROOT}\bin\ffmpeg_README.txt SectionEnd !endif Section "-Main_refreshShell" ${RefreshShell} SectionEnd !insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN !insertmacro MUI_DESCRIPTION_TEXT ${SEC_remove_shellex} "Remove previously installed Krita Shell Integration." !insertmacro MUI_DESCRIPTION_TEXT ${SEC_remove_old_version} "Remove previously installed Krita $KritaNsisVersion ($KritaNsisBitness-bit)." !insertmacro MUI_DESCRIPTION_TEXT ${SEC_product_main} "${KRITA_PRODUCTNAME} ${KRITA_VERSION_DISPLAY}$\r$\n$\r$\nVersion: ${KRITA_VERSION}" !insertmacro MUI_DESCRIPTION_TEXT ${SEC_shellex} "Shell Extension component to provide thumbnails and file properties display for Krita files.$\r$\n$\r$\nVersion: ${KRITASHELLEX_VERSION}" !ifdef HAS_FFMPEG !insertmacro MUI_DESCRIPTION_TEXT ${SEC_ffmpeg} "Install a bundled version of FFmpeg for exporting animations." !endif !insertmacro MUI_FUNCTION_DESCRIPTION_END Section "un.Shell Integration" ${If} $UninstallShellExStandalone == 1 push $R0 DetailPrint "Removing Krita Shell Integration..." SetDetailsPrint listonly ExecWait "$INSTDIR\shellex\uninstall.exe /S _?=$INSTDIR\shellex" $R0 ${If} $R0 != 0 ${IfNot} ${Silent} MessageBox MB_OK|MB_ICONSTOP "Failed to remove Krita Shell Integration. Please report this bug!" ${EndIf} SetDetailsPrint lastused SetDetailsPrint both DetailPrint "Failed to remove Krita Shell Integration." ${EndIf} Delete "$INSTDIR\shellex\uninstall.exe" RMDir /REBOOTOK "$INSTDIR\shellex" SetDetailsPrint lastused DetailPrint "Krita Shell Integration removed." pop $R0 ${Else} ${Krita_UnregisterShellExtension} ${If} ${RunningX64} ${Krita_UnregisterComComonents} 64 ${EndIf} ${Krita_UnregisterComComonents} 32 ${EndIf} SectionEnd Section "un.Main_associate" # TODO: Conditional, use install log ${If} $UninstallShellExStandalone != 1 ${Krita_UnregisterFileAssociation} ${EndIf} SectionEnd Section "un.Main_Shortcuts" Delete "$DESKTOP\Krita.lnk" !insertmacro MUI_STARTMENU_GETFOLDER Krita $KritaStartMenuFolder Delete "$SMPROGRAMS\$KritaStartMenuFolder\Tools\Uninstall ${KRITA_PRODUCTNAME}.lnk" RMDir "$SMPROGRAMS\$KritaStartMenuFolder\Tools" Delete "$SMPROGRAMS\$KritaStartMenuFolder\${KRITA_PRODUCTNAME}.lnk" RMDir "$SMPROGRAMS\$KritaStartMenuFolder" SectionEnd Section "un.${KRITA_PRODUCTNAME}" # TODO: Maybe switch to explicit file list or some sort of install log? RMDir /r $INSTDIR\bin RMDir /r $INSTDIR\lib RMDir /r $INSTDIR\share RMDir /r $INSTDIR\python SectionEnd Section "un.Thing" RMDir /REBOOTOK $INSTDIR\shellex DeleteRegKey HKLM "Software\Krita" DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${KRITA_UNINSTALL_REGKEY}" Delete $INSTDIR\uninstall.exe RMDir /REBOOTOK $INSTDIR SectionEnd Section "un.Main_refreshShell" ${RefreshShell} SectionEnd Function .onInit SetShellVarContext all !insertmacro SetSectionFlag ${SEC_product_main} ${SF_RO} !insertmacro SetSectionFlag ${SEC_product_main} ${SF_BOLD} !insertmacro SetSectionFlag ${SEC_remove_old_version} ${SF_RO} !ifdef HAS_FFMPEG !insertmacro SetSectionFlag ${SEC_ffmpeg} ${SF_RO} !endif StrCpy $CreateDesktopIcon 1 # Create desktop icon by default ${IfNot} ${AtLeastWin7} ${IfNot} ${Silent} MessageBox MB_OK|MB_ICONSTOP "${KRITA_PRODUCTNAME} ${KRITA_VERSION_DISPLAY} requires Windows 7 or above." ${EndIf} Abort ${EndIf} !ifdef KRITA_INSTALLER_64 ${If} ${RunningX64} SetRegView 64 ${Else} ${IfNot} ${Silent} MessageBox MB_OK|MB_ICONSTOP "You are running 32-bit Windows, but this installer installs Krita 64-bit which can only be installed on 64-bit Windows. Please download the 32-bit version on https://krita.org/" ${EndIf} Abort ${Endif} !else ${If} ${RunningX64} SetRegView 64 ${IfNot} ${Silent} MessageBox MB_YESNO|MB_ICONEXCLAMATION "You are trying to install 32-bit Krita on 64-bit Windows. You are strongly recommended to install the 64-bit version of Krita instead since it offers better performance.$\nIf you want to use the 32-bit version for testing, you should consider using the zip package instead.$\n$\nDo you still wish to install the 32-bit version of Krita?" \ /SD IDYES \ IDYES lbl_allow32on64 Abort ${EndIf} lbl_allow32on64: ${Endif} !endif # Detect other Krita versions ${DetectKritaMsi32bit} $KritaMsiProductX86 ${If} ${RunningX64} ${DetectKritaMsi64bit} $KritaMsiProductX64 ${IfKritaMsi3Alpha} $KritaMsiProductX64 ${IfNot} ${Silent} MessageBox MB_YESNO|MB_ICONQUESTION|MB_DEFBUTTON2 "Krita 3.0 Alpha 1 is installed. It must be removed before ${KRITA_PRODUCTNAME} ${KRITA_VERSION_DISPLAY} can be installed.$\nDo you wish to remove it now?" \ /SD IDYES \ IDYES lbl_removeKrita3alpha Abort ${EndIf} lbl_removeKrita3alpha: push $R0 ${MsiUninstall} $KritaMsiProductX64 $R0 ${If} $R0 != 0 ${IfNot} ${Silent} MessageBox MB_OK|MB_ICONSTOP "Failed to remove Krita 3.0 Alpha 1." ${EndIf} Abort ${EndIf} pop $R0 StrCpy $KritaMsiProductX64 "" ${ElseIf} $KritaMsiProductX64 != "" ${If} $KritaMsiProductX86 != "" ${IfNot} ${Silent} MessageBox MB_YESNO|MB_ICONQUESTION|MB_DEFBUTTON2 "Both 32-bit and 64-bit editions of Krita 2.9 or below are installed.$\nBoth must be removed before ${KRITA_PRODUCTNAME} ${KRITA_VERSION_DISPLAY} can be installed.$\nDo you want to remove them now?" \ /SD IDYES \ IDYES lbl_removeKritaBoth Abort ${EndIf} lbl_removeKritaBoth: push $R0 ${MsiUninstall} $KritaMsiProductX86 $R0 ${If} $R0 != 0 ${IfNot} ${Silent} MessageBox MB_OK|MB_ICONSTOP "Failed to remove Krita (32-bit)." ${EndIf} Abort ${EndIf} ${MsiUninstall} $KritaMsiProductX64 $R0 ${If} $R0 != 0 ${IfNot} ${Silent} MessageBox MB_OK|MB_ICONSTOP "Failed to remove Krita (64-bit)." ${EndIf} Abort ${EndIf} pop $R0 StrCpy $KritaMsiProductX86 "" StrCpy $KritaMsiProductX64 "" ${Else} ${IfNot} ${Silent} MessageBox MB_YESNO|MB_ICONQUESTION|MB_DEFBUTTON2 "Krita (64-bit) 2.9 or below is installed.$\nIt must be removed before ${KRITA_PRODUCTNAME} ${KRITA_VERSION_DISPLAY} can be installed.$\nDo you wish to remove it now?" \ /SD IDYES \ IDYES lbl_removeKritaX64 Abort ${EndIf} lbl_removeKritaX64: push $R0 ${MsiUninstall} $KritaMsiProductX64 $R0 ${If} $R0 != 0 ${IfNot} ${Silent} MessageBox MB_OK|MB_ICONSTOP "Failed to remove Krita (64-bit)." ${EndIf} Abort ${EndIf} pop $R0 StrCpy $KritaMsiProductX64 "" ${EndIf} ${EndIf} ${Endif} ${If} $KritaMsiProductX86 != "" ${IfNot} ${Silent} MessageBox MB_YESNO|MB_ICONQUESTION|MB_DEFBUTTON2 "Krita (32-bit) 2.9 or below is installed.$\nIt must be removed before ${KRITA_PRODUCTNAME} ${KRITA_VERSION_DISPLAY} can be installed.$\nDo you wish to remove it now?" \ /SD IDYES \ IDYES lbl_removeKritaX86 Abort ${EndIf} lbl_removeKritaX86: push $R0 ${MsiUninstall} $KritaMsiProductX86 $R0 ${If} $R0 != 0 ${IfNot} ${Silent} MessageBox MB_OK|MB_ICONSTOP "Failed to remove Krita (32-bit)." ${EndIf} Abort ${EndIf} pop $R0 StrCpy $KritaMsiProductX86 "" ${EndIf} ${DetectKritaNsis} $KritaNsisVersion $KritaNsisBitness $KritaNsisInstallLocation ${If} $KritaNsisVersion != "" push $R0 ${VersionCompare} "${KRITA_VERSION}" "$KritaNsisVersion" $R0 ${If} $R0 == 0 # Same version installed... probably ${If} $KritaNsisBitness == ${KRITA_INSTALLER_BITNESS} # Very likely the same version ${IfNot} ${Silent} MessageBox MB_OK|MB_ICONINFORMATION "It appears that ${KRITA_PRODUCTNAME} ${KRITA_VERSION_DISPLAY} is already installed.$\nThis setup will reinstall it." ${EndIf} ${Else} # Very likely the same version but different arch ${IfNot} ${Silent} !ifdef KRITA_INSTALLER_64 MessageBox MB_OK|MB_ICONINFORMATION "It appears that Krita 32-bit ${KRITA_VERSION_DISPLAY} is currently installed. This setup will replace it with the 64-bit version." !else MessageBox MB_OK|MB_ICONEXCLAMATION "It appears that Krita 64-bit ${KRITA_VERSION_DISPLAY} is currently installed. This setup will replace it with the 32-bit version." !endif ${EndIf} ${EndIf} ${ElseIf} $R0 == 1 # Upgrade ${If} $KritaNsisBitness == ${KRITA_INSTALLER_BITNESS} - # Slient about upgrade + # Silent about upgrade ${Else} # Upgrade but different arch ${IfNot} ${Silent} !ifdef KRITA_INSTALLER_64 MessageBox MB_OK|MB_ICONINFORMATION "It appears that Krita 32-bit ($KritaNsisVersion) is currently installed. This setup will replace it with the 64-bit version of Krita ${KRITA_VERSION_DISPLAY}." !else MessageBox MB_OK|MB_ICONEXCLAMATION "It appears that Krita 64-bit ($KritaNsisVersion) is currently installed. This setup will replace it with the 32-bit version of Krita ${KRITA_VERSION_DISPLAY}." !endif ${EndIf} ${EndIf} ${ElseIf} $R0 == 2 ${IfNot} ${Silent} MessageBox MB_OK|MB_ICONSTOP "It appears that a newer version of Krita $KritaNsisBitness-bit ($KritaNsisVersion) is currently installed. If you want to downgrade Krita to ${KRITA_VERSION_DISPLAY}, please uninstall the newer version manually before running this setup." ${EndIf} Abort ${Else} ${IfNot} ${Silent} MessageBox MB_OK|MB_ICONSTOP "Unexpected state" ${EndIf} Abort ${EndIf} !insertmacro SetSectionFlag ${SEC_remove_old_version} ${SF_SELECTED} # Detect if Krita is running... ${If} ${IsFileinUse} "$KritaNsisInstallLocation\bin\krita.exe" ${IfNot} ${Silent} MessageBox MB_OK|MB_ICONEXCLAMATION "Krita appears to be running. Please close Krita before running this installer." ${EndIf} Abort ${EndIf} pop $R0 ${Else} !insertmacro ClearSectionFlag ${SEC_remove_old_version} ${SF_SELECTED} SectionSetText ${SEC_remove_old_version} "" ${EndIf} # Detect standalone shell extension # TODO: Would it be possible to update Krita without replacing the standalone shellex? ClearErrors ReadRegStr $PrevShellExInstallLocation HKLM "Software\Krita\ShellExtension" "InstallLocation" #ReadRegStr $PrevShellExVersion HKLM "Software\Krita\ShellExtension" "Version" ReadRegDWORD $PrevShellExStandalone HKLM "Software\Krita\ShellExtension" "Standalone" #ReadRegStr $PrevShellExKritaExePath HKLM "Software\Krita\ShellExtension" "KritaExePath" ${If} ${Errors} # TODO: Assume no previous version installed or what? ${EndIf} ${If} $PrevShellExStandalone == 1 ${IfNot} ${Silent} MessageBox MB_YESNO|MB_ICONQUESTION "Krita Shell Integration was installed separately. It will be uninstalled automatically when installing Krita.$\nDo you want to continue?" \ /SD IDYES \ IDYES lbl_allowremoveshellex Abort ${EndIf} lbl_allowremoveshellex: #!insertmacro SetSectionFlag ${SEC_remove_shellex} ${SF_SELECTED} ${Else} #!insertmacro ClearSectionFlag ${SEC_remove_shellex} ${SF_SELECTED} #SectionSetText ${SEC_remove_shellex} "" ${EndIf} FunctionEnd Function un.onInit SetShellVarContext all !ifdef KRITA_INSTALLER_64 ${If} ${RunningX64} SetRegView 64 ${Else} Abort ${Endif} !else ${If} ${RunningX64} SetRegView 64 ${Endif} !endif ReadRegDWORD $UninstallShellExStandalone HKLM "Software\Krita\ShellExtension" "Standalone" ${If} ${IsFileinUse} "$INSTDIR\bin\krita.exe" ${IfNot} ${Silent} MessageBox MB_OK|MB_ICONEXCLAMATION "Krita appears to be running. Please close Krita before uninstalling." ${EndIf} Abort ${EndIf} FunctionEnd Function func_ShellExLicensePage_Init ${IfNot} ${SectionIsSelected} ${SEC_shellex} # Skip ShellEx license page if not selected Abort ${EndIf} FunctionEnd Var hwndChkDesktopIcon Function func_DesktopShortcutPage_Init push $R0 nsDialogs::Create 1018 pop $R0 ${If} $R0 == error Abort ${EndIf} !insertmacro MUI_HEADER_TEXT "Desktop Icon" "Configure desktop shortcut icon." ${NSD_CreateLabel} 0u 0u 300u 20u "You can choose to create a shortcut icon on the desktop for launching Krita." pop $R0 ${NSD_CreateCheckbox} 0u 20u 300u 10u "Create a desktop icon" pop $hwndChkDesktopIcon ${If} $CreateDesktopIcon == 1 ${NSD_Check} $hwndChkDesktopIcon ${Else} ${NSD_Uncheck} $hwndChkDesktopIcon ${EndIf} ${NSD_OnClick} $hwndChkDesktopIcon func_DesktopShortcutPage_CheckChange nsDialogs::Show pop $R0 FunctionEnd Function func_DesktopShortcutPage_CheckChange ${NSD_GetState} $hwndChkDesktopIcon $CreateDesktopIcon ${If} $CreateDesktopIcon == ${BST_CHECKED} StrCpy $CreateDesktopIcon 1 ${Else} StrCpy $CreateDesktopIcon 0 ${EndIf} FunctionEnd Function func_BeforeInstallPage_Init push $R0 nsDialogs::Create 1018 pop $R0 ${If} $R0 == error Abort ${EndIf} !insertmacro MUI_HEADER_TEXT "Confirm Installation" "Confirm installation of ${KRITA_PRODUCTNAME} ${KRITA_VERSION_DISPLAY}." ${NSD_CreateLabel} 0u 0u 300u 140u "Setup is ready to install ${KRITA_PRODUCTNAME} ${KRITA_VERSION_DISPLAY}. You may review the install options before you continue.$\r$\n$\r$\n$_CLICK" pop $R0 # TODO: Add install option summary for review? nsDialogs::Show pop $R0 FunctionEnd diff --git a/plugins/color/lcms2engine/colorprofiles/LcmsColorProfileContainer.cpp b/plugins/color/lcms2engine/colorprofiles/LcmsColorProfileContainer.cpp index 8172fa50f7..14858c958c 100644 --- a/plugins/color/lcms2engine/colorprofiles/LcmsColorProfileContainer.cpp +++ b/plugins/color/lcms2engine/colorprofiles/LcmsColorProfileContainer.cpp @@ -1,569 +1,569 @@ /* * This file is part of the KDE project * Copyright (c) 2000 Matthias Elter * 2001 John Califf * 2004 Boudewijn Rempt * Copyright (c) 2007 Thomas Zander * Copyright (c) 2007 Adrian Page * * 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 "LcmsColorProfileContainer.h" #include #include #include #include #include #include "kis_debug.h" class LcmsColorProfileContainer::Private { public: cmsHPROFILE profile; cmsColorSpaceSignature colorSpaceSignature; cmsProfileClassSignature deviceClass; QString productDescription; QString manufacturer; QString copyright; QString name; float version; IccColorProfile::Data *data {0}; bool valid {false}; bool suitableForOutput {false}; bool hasColorants; bool hasTRC; bool isLinear {false}; bool adaptedFromD50; cmsCIEXYZ mediaWhitePoint; cmsCIExyY whitePoint; cmsCIEXYZTRIPLE colorants; cmsToneCurve *redTRC {0}; cmsToneCurve *greenTRC {0}; cmsToneCurve *blueTRC {0}; cmsToneCurve *grayTRC {0}; cmsToneCurve *redTRCReverse {0}; cmsToneCurve *greenTRCReverse {0}; cmsToneCurve *blueTRCReverse {0}; cmsToneCurve *grayTRCReverse {0}; cmsUInt32Number defaultIntent; bool isPerceptualCLUT; bool isRelativeCLUT; bool isAbsoluteCLUT; bool isSaturationCLUT; bool isMatrixShaper; QByteArray uniqueId; }; LcmsColorProfileContainer::LcmsColorProfileContainer() : d(new Private()) { d->profile = 0; } LcmsColorProfileContainer::LcmsColorProfileContainer(IccColorProfile::Data *data) : d(new Private()) { d->data = data; d->profile = 0; init(); } QByteArray LcmsColorProfileContainer::lcmsProfileToByteArray(const cmsHPROFILE profile) { cmsUInt32Number bytesNeeded = 0; // Make a raw data image ready for saving cmsSaveProfileToMem(profile, 0, &bytesNeeded); // calc size QByteArray rawData; rawData.resize(bytesNeeded); if (rawData.size() >= (int)bytesNeeded) { cmsSaveProfileToMem(profile, rawData.data(), &bytesNeeded); // fill buffer } else { qWarning() << "Couldn't resize the profile buffer, system is probably running out of memory."; rawData.resize(0); } return rawData; } IccColorProfile *LcmsColorProfileContainer::createFromLcmsProfile(const cmsHPROFILE profile) { IccColorProfile *iccprofile = new IccColorProfile(lcmsProfileToByteArray(profile)); cmsCloseProfile(profile); return iccprofile; } LcmsColorProfileContainer::~LcmsColorProfileContainer() { cmsCloseProfile(d->profile); delete d; } #define _BUFFER_SIZE_ 1000 bool LcmsColorProfileContainer::init() { if (d->profile) { cmsCloseProfile(d->profile); } d->profile = cmsOpenProfileFromMem((void *)d->data->rawData().constData(), d->data->rawData().size()); #ifndef NDEBUG if (d->data->rawData().size() == 4096) { qWarning() << "Profile has a size of 4096, which is suspicious and indicates a possible misuse of QIODevice::read(int), check your code."; } #endif if (d->profile) { wchar_t buffer[_BUFFER_SIZE_]; d->colorSpaceSignature = cmsGetColorSpace(d->profile); d->deviceClass = cmsGetDeviceClass(d->profile); cmsGetProfileInfo(d->profile, cmsInfoDescription, cmsNoLanguage, cmsNoCountry, buffer, _BUFFER_SIZE_); d->name = QString::fromWCharArray(buffer); //apparently this should give us a localised string??? Not sure about this. cmsGetProfileInfo(d->profile, cmsInfoModel, cmsNoLanguage, cmsNoCountry, buffer, _BUFFER_SIZE_); d->productDescription = QString::fromWCharArray(buffer); cmsGetProfileInfo(d->profile, cmsInfoManufacturer, cmsNoLanguage, cmsNoCountry, buffer, _BUFFER_SIZE_); d->manufacturer = QString::fromWCharArray(buffer); cmsGetProfileInfo(d->profile, cmsInfoCopyright, cmsNoLanguage, cmsNoCountry, buffer, _BUFFER_SIZE_); d->copyright = QString::fromWCharArray(buffer); cmsProfileClassSignature profile_class; profile_class = cmsGetDeviceClass(d->profile); d->valid = (profile_class != cmsSigNamedColorClass); //This is where obtain the whitepoint, and convert it to the actual white point of the profile in the case a Chromatic adaption tag is //present. This is necessary for profiles following the v4 spec. cmsCIEXYZ baseMediaWhitePoint;//dummy to hold copy of mediawhitepoint if this is modified by chromatic adaption. if (cmsIsTag(d->profile, cmsSigMediaWhitePointTag)) { d->mediaWhitePoint = *((cmsCIEXYZ *)cmsReadTag(d->profile, cmsSigMediaWhitePointTag)); baseMediaWhitePoint = d->mediaWhitePoint; cmsXYZ2xyY(&d->whitePoint, &d->mediaWhitePoint); if (cmsIsTag(d->profile, cmsSigChromaticAdaptationTag)) { //the chromatic adaption tag represent a matrix from the actual white point of the profile to D50. cmsCIEXYZ *CAM1 = (cmsCIEXYZ *)cmsReadTag(d->profile, cmsSigChromaticAdaptationTag); //We first put all our data into structures we can manipulate. double d3dummy [3] = {d->mediaWhitePoint.X, d->mediaWhitePoint.Y, d->mediaWhitePoint.Z}; QGenericMatrix<1, 3, double> whitePointMatrix(d3dummy); QTransform invertDummy(CAM1[0].X, CAM1[0].Y, CAM1[0].Z, CAM1[1].X, CAM1[1].Y, CAM1[1].Z, CAM1[2].X, CAM1[2].Y, CAM1[2].Z); - //we then abuse QTransform's invert function because it probably does matrix invertion 20 times better than I can program. + //we then abuse QTransform's invert function because it probably does matrix inversion 20 times better than I can program. //if the matrix is uninvertable, invertedDummy will be an identity matrix, which for us means that it won't give any noticeble //effect when we start multiplying. QTransform invertedDummy = invertDummy.inverted(); //we then put the QTransform into a generic 3x3 matrix. double d9dummy [9] = {invertedDummy.m11(), invertedDummy.m12(), invertedDummy.m13(), invertedDummy.m21(), invertedDummy.m22(), invertedDummy.m23(), invertedDummy.m31(), invertedDummy.m32(), invertedDummy.m33() }; QGenericMatrix<3, 3, double> chromaticAdaptionMatrix(d9dummy); //multiplying our inverted adaption matrix with the whitepoint gives us the right whitepoint. QGenericMatrix<1, 3, double> result = chromaticAdaptionMatrix * whitePointMatrix; //and then we pour the matrix into the whitepoint variable. Generic matrix does row/column for indices even though it //uses column/row for initialising. d->mediaWhitePoint.X = result(0, 0); d->mediaWhitePoint.Y = result(1, 0); d->mediaWhitePoint.Z = result(2, 0); cmsXYZ2xyY(&d->whitePoint, &d->mediaWhitePoint); } } //This is for RGB profiles, but it only works for matrix profiles. Need to design it to work with non-matrix profiles. if (cmsIsTag(d->profile, cmsSigRedColorantTag)) { cmsCIEXYZTRIPLE tempColorants; tempColorants.Red = *((cmsCIEXYZ *)cmsReadTag(d->profile, cmsSigRedColorantTag)); tempColorants.Green = *((cmsCIEXYZ *)cmsReadTag(d->profile, cmsSigGreenColorantTag)); tempColorants.Blue = *((cmsCIEXYZ *)cmsReadTag(d->profile, cmsSigBlueColorantTag)); //convert to d65, this is useless. cmsAdaptToIlluminant(&d->colorants.Red, &baseMediaWhitePoint, &d->mediaWhitePoint, &tempColorants.Red); cmsAdaptToIlluminant(&d->colorants.Green, &baseMediaWhitePoint, &d->mediaWhitePoint, &tempColorants.Green); cmsAdaptToIlluminant(&d->colorants.Blue, &baseMediaWhitePoint, &d->mediaWhitePoint, &tempColorants.Blue); //d->colorants = tempColorants; d->hasColorants = true; } else { //qDebug()<name<<": has no colorants"; d->hasColorants = false; } //retrieve TRC. if (cmsIsTag(d->profile, cmsSigRedTRCTag) && cmsIsTag(d->profile, cmsSigBlueTRCTag) && cmsIsTag(d->profile, cmsSigGreenTRCTag)) { d->redTRC = ((cmsToneCurve *)cmsReadTag (d->profile, cmsSigRedTRCTag)); d->greenTRC = ((cmsToneCurve *)cmsReadTag (d->profile, cmsSigGreenTRCTag)); d->blueTRC = ((cmsToneCurve *)cmsReadTag (d->profile, cmsSigBlueTRCTag)); if (d->redTRC) d->redTRCReverse = cmsReverseToneCurve(d->redTRC); if (d->greenTRC) d->greenTRCReverse = cmsReverseToneCurve(d->greenTRC); if (d->blueTRC) d->blueTRCReverse = cmsReverseToneCurve(d->blueTRC); d->hasTRC = (d->redTRC && d->greenTRC && d->blueTRC && d->redTRCReverse && d->greenTRCReverse && d->blueTRCReverse); if (d->hasTRC) d->isLinear = cmsIsToneCurveLinear(d->redTRC) && cmsIsToneCurveLinear(d->greenTRC) && cmsIsToneCurveLinear(d->blueTRC); } else if (cmsIsTag(d->profile, cmsSigGrayTRCTag)) { d->grayTRC = ((cmsToneCurve *)cmsReadTag (d->profile, cmsSigGrayTRCTag)); if (d->grayTRC) d->grayTRCReverse = cmsReverseToneCurve(d->grayTRC); d->hasTRC = (d->grayTRC && d->grayTRCReverse); if (d->hasTRC) d->isLinear = cmsIsToneCurveLinear(d->grayTRC); } else { d->hasTRC = false; } // Check if the profile can convert (something->this) d->suitableForOutput = cmsIsMatrixShaper(d->profile) || (cmsIsCLUT(d->profile, INTENT_PERCEPTUAL, LCMS_USED_AS_INPUT) && cmsIsCLUT(d->profile, INTENT_PERCEPTUAL, LCMS_USED_AS_OUTPUT)); d->version = cmsGetProfileVersion(d->profile); d->defaultIntent = cmsGetHeaderRenderingIntent(d->profile); d->isMatrixShaper = cmsIsMatrixShaper(d->profile); d->isPerceptualCLUT = cmsIsCLUT(d->profile, INTENT_PERCEPTUAL, LCMS_USED_AS_INPUT); d->isSaturationCLUT = cmsIsCLUT(d->profile, INTENT_SATURATION, LCMS_USED_AS_INPUT); d->isAbsoluteCLUT = cmsIsCLUT(d->profile, INTENT_SATURATION, LCMS_USED_AS_INPUT); d->isRelativeCLUT = cmsIsCLUT(d->profile, INTENT_RELATIVE_COLORIMETRIC, LCMS_USED_AS_INPUT); return true; } return false; } cmsHPROFILE LcmsColorProfileContainer::lcmsProfile() const { return d->profile; } cmsColorSpaceSignature LcmsColorProfileContainer::colorSpaceSignature() const { return d->colorSpaceSignature; } cmsProfileClassSignature LcmsColorProfileContainer::deviceClass() const { return d->deviceClass; } QString LcmsColorProfileContainer::manufacturer() const { return d->manufacturer; } QString LcmsColorProfileContainer::copyright() const { return d->copyright; } bool LcmsColorProfileContainer::valid() const { return d->valid; } float LcmsColorProfileContainer::version() const { return d->version; } bool LcmsColorProfileContainer::isSuitableForOutput() const { return d->suitableForOutput; } bool LcmsColorProfileContainer::isSuitableForPrinting() const { return deviceClass() == cmsSigOutputClass; } bool LcmsColorProfileContainer::isSuitableForDisplay() const { return deviceClass() == cmsSigDisplayClass; } bool LcmsColorProfileContainer::supportsPerceptual() const { return d->isPerceptualCLUT; } bool LcmsColorProfileContainer::supportsSaturation() const { return d->isSaturationCLUT; } bool LcmsColorProfileContainer::supportsAbsolute() const { return d->isAbsoluteCLUT;//LCMS2 doesn't convert matrix shapers via absolute intent, because of V4 workflow. } bool LcmsColorProfileContainer::supportsRelative() const { if (d->isRelativeCLUT || d->isMatrixShaper){ return true; } return false; } bool LcmsColorProfileContainer::hasColorants() const { return d->hasColorants; } bool LcmsColorProfileContainer::hasTRC() const { return d->hasTRC; } bool LcmsColorProfileContainer::isLinear() const { return d->isLinear; } QVector LcmsColorProfileContainer::getColorantsXYZ() const { QVector colorants(9); colorants[0] = d->colorants.Red.X; colorants[1] = d->colorants.Red.Y; colorants[2] = d->colorants.Red.Z; colorants[3] = d->colorants.Green.X; colorants[4] = d->colorants.Green.Y; colorants[5] = d->colorants.Green.Z; colorants[6] = d->colorants.Blue.X; colorants[7] = d->colorants.Blue.Y; colorants[8] = d->colorants.Blue.Z; return colorants; } QVector LcmsColorProfileContainer::getColorantsxyY() const { cmsCIEXYZ temp1; cmsCIExyY temp2; QVector colorants(9); temp1.X = d->colorants.Red.X; temp1.Y = d->colorants.Red.Y; temp1.Z = d->colorants.Red.Z; cmsXYZ2xyY(&temp2, &temp1); colorants[0] = temp2.x; colorants[1] = temp2.y; colorants[2] = temp2.Y; temp1.X = d->colorants.Green.X; temp1.Y = d->colorants.Green.Y; temp1.Z = d->colorants.Green.Z; cmsXYZ2xyY(&temp2, &temp1); colorants[3] = temp2.x; colorants[4] = temp2.y; colorants[5] = temp2.Y; temp1.X = d->colorants.Blue.X; temp1.Y = d->colorants.Blue.Y; temp1.Z = d->colorants.Blue.Z; cmsXYZ2xyY(&temp2, &temp1); colorants[6] = temp2.x; colorants[7] = temp2.y; colorants[8] = temp2.Y; return colorants; } QVector LcmsColorProfileContainer::getWhitePointXYZ() const { QVector tempWhitePoint(3); tempWhitePoint[0] = d->mediaWhitePoint.X; tempWhitePoint[1] = d->mediaWhitePoint.Y; tempWhitePoint[2] = d->mediaWhitePoint.Z; return tempWhitePoint; } QVector LcmsColorProfileContainer::getWhitePointxyY() const { QVector tempWhitePoint(3); tempWhitePoint[0] = d->whitePoint.x; tempWhitePoint[1] = d->whitePoint.y; tempWhitePoint[2] = d->whitePoint.Y; return tempWhitePoint; } QVector LcmsColorProfileContainer::getEstimatedTRC() const { QVector TRCtriplet(3); if (d->hasColorants) { if (cmsIsToneCurveLinear(d->redTRC)) { TRCtriplet[0] = 1.0; } else { TRCtriplet[0] = cmsEstimateGamma(d->redTRC, 0.01); } if (cmsIsToneCurveLinear(d->greenTRC)) { TRCtriplet[1] = 1.0; } else { TRCtriplet[1] = cmsEstimateGamma(d->greenTRC, 0.01); } if (cmsIsToneCurveLinear(d->blueTRC)) { TRCtriplet[2] = 1.0; } else { TRCtriplet[2] = cmsEstimateGamma(d->blueTRC, 0.01); } } else { if (cmsIsTag(d->profile, cmsSigGrayTRCTag)) { if (cmsIsToneCurveLinear(d->grayTRC)) { TRCtriplet.fill(1.0); } else { TRCtriplet.fill(cmsEstimateGamma(d->grayTRC, 0.01)); } } else { TRCtriplet.fill(1.0); } } return TRCtriplet; } void LcmsColorProfileContainer::LinearizeFloatValue(QVector & Value) const { if (d->hasColorants) { if (!cmsIsToneCurveLinear(d->redTRC)) { Value[0] = cmsEvalToneCurveFloat(d->redTRC, Value[0]); } if (!cmsIsToneCurveLinear(d->greenTRC)) { Value[1] = cmsEvalToneCurveFloat(d->greenTRC, Value[1]); } if (!cmsIsToneCurveLinear(d->blueTRC)) { Value[2] = cmsEvalToneCurveFloat(d->blueTRC, Value[2]); } } else { if (cmsIsTag(d->profile, cmsSigGrayTRCTag)) { Value[0] = cmsEvalToneCurveFloat(d->grayTRC, Value[0]); } } } void LcmsColorProfileContainer::DelinearizeFloatValue(QVector & Value) const { if (d->hasColorants) { if (!cmsIsToneCurveLinear(d->redTRC)) { Value[0] = cmsEvalToneCurveFloat(d->redTRCReverse, Value[0]); } if (!cmsIsToneCurveLinear(d->greenTRC)) { Value[1] = cmsEvalToneCurveFloat(d->greenTRCReverse, Value[1]); } if (!cmsIsToneCurveLinear(d->blueTRC)) { Value[2] = cmsEvalToneCurveFloat(d->blueTRCReverse, Value[2]); } } else { if (cmsIsTag(d->profile, cmsSigGrayTRCTag)) { Value[0] = cmsEvalToneCurveFloat(d->grayTRCReverse, Value[0]); } } } void LcmsColorProfileContainer::LinearizeFloatValueFast(QVector & Value) const { const qreal scale = 65535.0; const qreal invScale = 1.0 / scale; if (d->hasColorants) { //we can only reliably delinearise in the 0-1.0 range, outside of that leave the value alone. QVector TRCtriplet(3); TRCtriplet[0] = Value[0] * scale; TRCtriplet[1] = Value[1] * scale; TRCtriplet[2] = Value[2] * scale; if (!cmsIsToneCurveLinear(d->redTRC) && Value[0]<1.0) { TRCtriplet[0] = cmsEvalToneCurve16(d->redTRC, TRCtriplet[0]); Value[0] = TRCtriplet[0] * invScale; } if (!cmsIsToneCurveLinear(d->greenTRC) && Value[1]<1.0) { TRCtriplet[1] = cmsEvalToneCurve16(d->greenTRC, TRCtriplet[1]); Value[1] = TRCtriplet[1] * invScale; } if (!cmsIsToneCurveLinear(d->blueTRC) && Value[2]<1.0) { TRCtriplet[2] = cmsEvalToneCurve16(d->blueTRC, TRCtriplet[2]); Value[2] = TRCtriplet[2] * invScale; } } else { if (cmsIsTag(d->profile, cmsSigGrayTRCTag) && Value[0]<1.0) { quint16 newValue = cmsEvalToneCurve16(d->grayTRC, Value[0] * scale); Value[0] = newValue * invScale; } } } void LcmsColorProfileContainer::DelinearizeFloatValueFast(QVector & Value) const { const qreal scale = 65535.0; const qreal invScale = 1.0 / scale; if (d->hasColorants) { //we can only reliably delinearise in the 0-1.0 range, outside of that leave the value alone. QVector TRCtriplet(3); TRCtriplet[0] = Value[0] * scale; TRCtriplet[1] = Value[1] * scale; TRCtriplet[2] = Value[2] * scale; if (!cmsIsToneCurveLinear(d->redTRC) && Value[0]<1.0) { TRCtriplet[0] = cmsEvalToneCurve16(d->redTRCReverse, TRCtriplet[0]); Value[0] = TRCtriplet[0] * invScale; } if (!cmsIsToneCurveLinear(d->greenTRC) && Value[1]<1.0) { TRCtriplet[1] = cmsEvalToneCurve16(d->greenTRCReverse, TRCtriplet[1]); Value[1] = TRCtriplet[1] * invScale; } if (!cmsIsToneCurveLinear(d->blueTRC) && Value[2]<1.0) { TRCtriplet[2] = cmsEvalToneCurve16(d->blueTRCReverse, TRCtriplet[2]); Value[2] = TRCtriplet[2] * invScale; } } else { if (cmsIsTag(d->profile, cmsSigGrayTRCTag) && Value[0]<1.0) { quint16 newValue = cmsEvalToneCurve16(d->grayTRCReverse, Value[0] * scale); Value[0] = newValue * invScale; } } } QString LcmsColorProfileContainer::name() const { return d->name; } QString LcmsColorProfileContainer::info() const { return d->productDescription; } QByteArray LcmsColorProfileContainer::getProfileUniqueId() const { if (d->uniqueId.isEmpty() && d->profile) { QByteArray id(sizeof(cmsProfileID), 0); cmsGetHeaderProfileID(d->profile, (quint8*)id.data()); bool isNull = std::all_of(id.constBegin(), id.constEnd(), [](char c) {return c == 0;}); if (isNull) { if (cmsMD5computeID(d->profile)) { cmsGetHeaderProfileID(d->profile, (quint8*)id.data()); isNull = false; } } if (!isNull) { d->uniqueId = id; } } return d->uniqueId; } diff --git a/plugins/dockers/smallcolorselector/KisClickableGLImageWidget.cpp b/plugins/dockers/smallcolorselector/KisClickableGLImageWidget.cpp index ad19466b6e..90d3fd39f3 100644 --- a/plugins/dockers/smallcolorselector/KisClickableGLImageWidget.cpp +++ b/plugins/dockers/smallcolorselector/KisClickableGLImageWidget.cpp @@ -1,158 +1,158 @@ /* * Copyright (c) 2019 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 "KisClickableGLImageWidget.h" #include #include #include "kis_algebra_2d.h" #include KisClickableGLImageWidget::KisClickableGLImageWidget(QWidget *parent) : KisGLImageWidget(parent) { } KisClickableGLImageWidget::KisClickableGLImageWidget(KisSurfaceColorSpace colorSpace, QWidget *parent) : KisGLImageWidget(colorSpace, parent) { } KisClickableGLImageWidget::~KisClickableGLImageWidget() { } void KisClickableGLImageWidget::setHandlePaintingStrategy(HandlePaintingStrategy *strategy) { m_handleStrategy.reset(strategy); } void KisClickableGLImageWidget::setUseHandleOpacity(bool value) { m_useHandleOpacity = value; update(); } QPointF KisClickableGLImageWidget::normalizedPos() const { return m_normalizedClickPoint; } void KisClickableGLImageWidget::setNormalizedPos(const QPointF &pos, bool update) { m_normalizedClickPoint = KisAlgebra2D::clampPoint(pos, QRectF(0,0,1.0,1.0)); if (update) { this->update(); } } void KisClickableGLImageWidget::paintEvent(QPaintEvent *event) { KisGLImageWidget::paintEvent(event); if (m_handleStrategy) { QPainter p(this); m_handleStrategy->drawHandle(&p, m_normalizedClickPoint, rect(), m_useHandleOpacity); } } void KisClickableGLImageWidget::mousePressEvent(QMouseEvent *event) { KisGLImageWidget::mousePressEvent(event); if (!event->isAccepted()) { event->accept(); m_normalizedClickPoint = normalizePoint(event->localPos()); emit selected(m_normalizedClickPoint); if (m_handleStrategy) { update(); } } } void KisClickableGLImageWidget::mouseReleaseEvent(QMouseEvent *event) { KisGLImageWidget::mouseReleaseEvent(event); if (!event->isAccepted()) { event->accept(); m_normalizedClickPoint = normalizePoint(event->localPos()); emit selected(m_normalizedClickPoint); if (m_handleStrategy) { update(); } } } void KisClickableGLImageWidget::mouseMoveEvent(QMouseEvent *event) { KisGLImageWidget::mouseMoveEvent(event); if (!event->isAccepted()) { event->accept(); m_normalizedClickPoint = normalizePoint(event->localPos()); emit selected(m_normalizedClickPoint); if (m_handleStrategy) { update(); } } } QPointF KisClickableGLImageWidget::normalizePoint(const QPointF &pos) const { const QPointF croppedPoint = KisAlgebra2D::clampPoint(pos, rect()); return QPointF(croppedPoint.x() / width(), croppedPoint.y() / height()); } namespace { QPen outerHandlePen(bool useOpacity) { - // opacity works inexpectedly in HDR mode, so let the user switch it off + // opacity works unexpectedly in HDR mode, so let the user switch it off return QPen(QColor(0, 0, 0, useOpacity ? 180 : 255), 0); } QPen innerHandlePen(bool useOpacity) { - // opacity works inexpectedly in HDR mode, so let the user switch it off + // opacity works unexpectedly in HDR mode, so let the user switch it off return QPen(QColor(255, 255, 255, useOpacity ? 180 : 255), 0); } } void KisClickableGLImageWidget::VerticalLineHandleStrategy::drawHandle(QPainter *p, const QPointF &normalizedPoint, const QRect &rect, bool useOpacity) { const QPointF pos = KisAlgebra2D::relativeToAbsolute(normalizedPoint, rect); const int x = std::floor(pos.x()); p->setPen(outerHandlePen(useOpacity)); p->drawLine(x, rect.top(), x, rect.bottom()); p->setPen(innerHandlePen(useOpacity)); p->drawLine(x + 1, rect.top(), x + 1, rect.bottom()); } void KisClickableGLImageWidget::CircularHandleStrategy::drawHandle(QPainter *p, const QPointF &normalizedPoint, const QRect &rect, bool useOpacity) { const QPointF pos = KisAlgebra2D::relativeToAbsolute(normalizedPoint, rect); p->setRenderHint(QPainter::Antialiasing); p->setPen(outerHandlePen(useOpacity)); p->drawEllipse(pos, 5, 5); p->setPen(innerHandlePen(useOpacity)); p->drawEllipse(pos, 4, 4); } diff --git a/plugins/filters/gaussianhighpass/gaussianhighpass_filter.cpp b/plugins/filters/gaussianhighpass/gaussianhighpass_filter.cpp index 584340e05b..c258f3efc7 100644 --- a/plugins/filters/gaussianhighpass/gaussianhighpass_filter.cpp +++ b/plugins/filters/gaussianhighpass/gaussianhighpass_filter.cpp @@ -1,124 +1,124 @@ /* * This file is part of Krita * * Copyright (c) 2019 Miguel Lopez * * 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 //MSVC requires that Vc come first #include "gaussianhighpass_filter.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_lod_transform.h" #include #include "wdg_gaussianhighpass.h" #include "ui_wdggaussianhighpass.h" #include "KoColorSpaceTraits.h" #include KisGaussianHighPassFilter::KisGaussianHighPassFilter() : KisFilter(id(), FiltersCategoryEdgeDetectionId, i18n("&Gaussian High Pass...")) { setSupportsPainting(true); setSupportsAdjustmentLayers(true); setSupportsThreading(true); setSupportsLevelOfDetail(true); setColorSpaceIndependence(FULLY_INDEPENDENT); } KisConfigWidget * KisGaussianHighPassFilter::createConfigurationWidget(QWidget* parent, const KisPaintDeviceSP, bool /* useForMasks */) const { return new KisWdgGaussianHighPass(parent); } KisFilterConfigurationSP KisGaussianHighPassFilter::defaultConfiguration() const { KisFilterConfigurationSP config = factoryConfiguration(); config->setProperty("blurAmount", 1); return config; } void KisGaussianHighPassFilter::processImpl(KisPaintDeviceSP device, const QRect& applyRect, const KisFilterConfigurationSP _config, KoUpdater * ) const { QPointer convolutionUpdater = 0; KisFilterConfigurationSP config = _config ? _config : new KisFilterConfiguration(id().id(), 1); QVariant value; KisLodTransformScalar t(device); const qreal blurAmount = t.scale(config->getProperty("blurAmount", value) ? value.toDouble() : 1.0); QBitArray channelFlags = config->channelFlags(); const QRect gaussNeedRect = this->neededRect(applyRect, config, device->defaultBounds()->currentLevelOfDetail()); KisCachedPaintDevice::Guard d1(device, m_cachedPaintDevice); KisPaintDeviceSP blur = d1.device(); KisPainter::copyAreaOptimizedOldData(gaussNeedRect.topLeft(), device, blur, gaussNeedRect); KisGaussianKernel::applyGaussian(blur, applyRect, blurAmount, blurAmount, channelFlags, convolutionUpdater, - true); // make sure we cerate an internal transaction on temp device + true); // make sure we craate an internal transaction on temp device KisPainter painter(device); painter.setCompositeOp(blur->colorSpace()->compositeOp(COMPOSITE_GRAIN_EXTRACT)); painter.bitBlt(applyRect.topLeft(), blur, applyRect); painter.end(); } QRect KisGaussianHighPassFilter::neededRect(const QRect & rect, const KisFilterConfigurationSP config, int lod) const { KisLodTransformScalar t(lod); QVariant value; const int halfSize = config->getProperty("blurAmount", value) ? KisGaussianKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; return rect.adjusted( -halfSize * 2, -halfSize * 2, halfSize * 2, halfSize * 2); } QRect KisGaussianHighPassFilter::changedRect(const QRect & rect, const KisFilterConfigurationSP config, int lod) const { KisLodTransformScalar t(lod); QVariant value; const int halfSize = config->getProperty("blurAmount", value) ? KisGaussianKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; return rect.adjusted( -halfSize, -halfSize, halfSize, halfSize); } diff --git a/plugins/python/comics_project_management_tools/comics_metadata_dialog.py b/plugins/python/comics_project_management_tools/comics_metadata_dialog.py index e9ebc923aa..c73b67fd37 100644 --- a/plugins/python/comics_project_management_tools/comics_metadata_dialog.py +++ b/plugins/python/comics_project_management_tools/comics_metadata_dialog.py @@ -1,784 +1,784 @@ """ 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 metadata editor that helps out setting the proper metadata """ import sys import os # For finding the script location. import csv import re import types from pathlib import Path # For reading all the files in a directory. from PyQt5.QtGui import QStandardItem, QStandardItemModel, QImage, QIcon, QPixmap, QPainter, QPalette, QFontDatabase from PyQt5.QtWidgets import QComboBox, QCompleter, QStyledItemDelegate, QLineEdit, QDialog, QDialogButtonBox, QVBoxLayout, QFormLayout, QTabWidget, QWidget, QPlainTextEdit, QHBoxLayout, QSpinBox, QDateEdit, QPushButton, QLabel, QTableView from PyQt5.QtCore import QDir, QLocale, QStringListModel, Qt, QDate, QSize, QUuid """ multi entry completer cobbled together from the two examples on stackoverflow:3779720 This allows us to let people type in comma-separated lists and get completion for those. """ class multi_entry_completer(QCompleter): punctuation = "," def __init__(self, parent=None): super(QCompleter, self).__init__(parent) def pathFromIndex(self, index): path = QCompleter.pathFromIndex(self, index) string = str(self.widget().text()) split = string.split(self.punctuation) if len(split) > 1: path = "%s, %s" % (",".join(split[:-1]), path) return path def splitPath(self, path): split = str(path.split(self.punctuation)[-1]) if split.startswith(" "): split = split[1:] if split.endswith(" "): split = split[:-1] return [split] """ Language combobox that can take locale codes and get the right language for it and visa-versa. """ class language_combo_box(QComboBox): languageList = [] codesList = [] def __init__(self, parent=None): super(QComboBox, self).__init__(parent) for i in range(1, 357): locale = QLocale(i) if locale and QLocale.languageToString(locale.language()) != "C": codeName = locale.name().split("_")[0] if codeName not in self.codesList: self.codesList.append(codeName) self.codesList.sort() for lang in self.codesList: locale = QLocale(lang) if locale: languageName = QLocale.languageToString(locale.language()) self.languageList.append(languageName.title()) self.setIconSize(QSize(32, 22)) codeIcon = QImage(self.iconSize(), QImage.Format_ARGB32) painter = QPainter(codeIcon) painter.setBrush(Qt.transparent) codeIcon.fill(Qt.transparent) font = QFontDatabase().systemFont(QFontDatabase.FixedFont) painter.setFont(font) painter.setPen(self.palette().color(QPalette.Text)) painter.drawText(codeIcon.rect(), Qt.AlignCenter,lang) painter.end() self.addItem(QIcon(QPixmap.fromImage(codeIcon)), languageName.title()) def codeForCurrentEntry(self): if self.currentText() in self.languageList: return self.codesList[self.languageList.index(self.currentText())] def setEntryToCode(self, code): if (code == "C" and "en" in self.codesList): self.setCurrentIndex(self.codesList.index("en")) if code in self.codesList: self.setCurrentIndex(self.codesList.index(code)) class country_combo_box(QComboBox): countryList = [] codesList = [] def __init__(self, parent=None): super(QComboBox, self).__init__(parent) def set_country_for_locale(self, languageCode): self.clear() self.codesList = [] self.countryList = [] for locale in QLocale.matchingLocales(QLocale(languageCode).language(), QLocale.AnyScript, QLocale.AnyCountry): codeName = locale.name().split("_")[-1] if codeName not in self.codesList: self.codesList.append(codeName) self.codesList.sort() for country in self.codesList: locale = QLocale(languageCode+"-"+country) if locale: countryName = locale.nativeCountryName() self.countryList.append(countryName.title()) self.setIconSize(QSize(32, 22)) codeIcon = QImage(self.iconSize(), QImage.Format_ARGB32) painter = QPainter(codeIcon) painter.setBrush(Qt.transparent) codeIcon.fill(Qt.transparent) font = QFontDatabase().systemFont(QFontDatabase.FixedFont) painter.setFont(font) painter.setPen(self.palette().color(QPalette.Text)) painter.drawText(codeIcon.rect(), Qt.AlignCenter,country) painter.end() self.addItem(QIcon(QPixmap.fromImage(codeIcon)), countryName.title()) def codeForCurrentEntry(self): if self.currentText() in self.countryList: return self.codesList[self.countryList.index(self.currentText())] def setEntryToCode(self, code): if code == "C": self.setCurrentIndex(0) elif code in self.codesList: self.setCurrentIndex(self.codesList.index(code)) """ A combobox that fills up with licenses from a CSV, and also sets tooltips from that csv. """ class license_combo_box(QComboBox): def __init__(self, parent=None): super(QComboBox, self).__init__(parent) mainP = os.path.dirname(__file__) languageP = os.path.join(mainP, "LicenseList.csv") model = QStandardItemModel() if (os.path.exists(languageP)): file = open(languageP, "r", newline="", encoding="utf8") languageReader = csv.reader(file) for row in languageReader: license = QStandardItem(row[0]) license.setToolTip(row[1]) model.appendRow(license) file.close() self.setModel(model) """ Allows us to set completers on the author roles. """ class author_delegate(QStyledItemDelegate): completerStrings = [] completerColumn = 0 languageColumn = 0 def __init__(self, parent=None): super(QStyledItemDelegate, self).__init__(parent) def setCompleterData(self, completerStrings=[str()], completerColumn=0): self.completerStrings = completerStrings self.completerColumn = completerColumn def setLanguageData(self, languageColumn=0): self.languageColumn = languageColumn def createEditor(self, parent, option, index): if index.column() != self.languageColumn: editor = QLineEdit(parent) else: editor = QComboBox(parent) editor.addItem("") for i in range(2, 356): if QLocale(i, QLocale.AnyScript, QLocale.AnyCountry) is not None: languagecode = QLocale(i, QLocale.AnyScript, QLocale.AnyCountry).name().split("_")[0] if languagecode != "C": editor.addItem(languagecode) editor.model().sort(0) if index.column() == self.completerColumn: editor.setCompleter(QCompleter(self.completerStrings)) editor.completer().setCaseSensitivity(False) return editor """ A comic project metadata editing dialog that can take our config diactionary and set all the relevant information. To help our user, the dialog loads up lists of keywords to populate several autocompletion methods. """ class comic_meta_data_editor(QDialog): configGroup = "ComicsProjectManagementTools" # Translatable genre dictionary that has it's translated entries added to the genrelist and from which the untranslated items are taken. acbfGenreList = {"science_fiction": str(i18n("Science Fiction")), "fantasy": str(i18n("Fantasy")), "adventure": str(i18n("Adventure")), "horror": str(i18n("Horror")), "mystery": str(i18n("Mystery")), "crime": str(i18n("Crime")), "military": str(i18n("Military")), "real_life": str(i18n("Real Life")), "superhero": str(i18n("Superhero")), "humor": str(i18n("Humor")), "western": str(i18n("Western")), "manga": str(i18n("Manga")), "politics": str(i18n("Politics")), "caricature": str(i18n("Caricature")), "sports": str(i18n("Sports")), "history": str(i18n("History")), "biography": str(i18n("Biography")), "education": str(i18n("Education")), "computer": str(i18n("Computer")), "religion": str(i18n("Religion")), "romance": str(i18n("Romance")), "children": str(i18n("Children")), "non-fiction": str(i18n("Non Fiction")), "adult": str(i18n("Adult")), "alternative": str(i18n("Alternative")), "artbook": str(i18n("Artbook")), "other": str(i18n("Other"))} acbfAuthorRolesList = {"Writer": str(i18n("Writer")), "Adapter": str(i18n("Adapter")), "Artist": str(i18n("Artist")), "Penciller": str(i18n("Penciller")), "Inker": str(i18n("Inker")), "Colorist": str(i18n("Colorist")), "Letterer": str(i18n("Letterer")), "Cover Artist": str(i18n("Cover Artist")), "Photographer": str(i18n("Photographer")), "Editor": str(i18n("Editor")), "Assistant Editor": str(i18n("Assistant Editor")), "Designer": str(i18n("Designer")), "Translator": str(i18n("Translator")), "Other": str(i18n("Other"))} def __init__(self): super().__init__() # Get the keys for the autocompletion. self.genreKeysList = [] self.characterKeysList = [] self.ratingKeysList = {} self.formatKeysList = [] self.otherKeysList = [] self.authorRoleList = [] for g in self.acbfGenreList.values(): self.genreKeysList.append(g) for r in self.acbfAuthorRolesList.values(): self.authorRoleList.append(r) mainP = Path(os.path.abspath(__file__)).parent self.get_auto_completion_keys(mainP) extraKeyP = Path(QDir.homePath()) / Application.readSetting(self.configGroup, "extraKeysLocation", str()) self.get_auto_completion_keys(extraKeyP) # Setup the dialog. self.setLayout(QVBoxLayout()) mainWidget = QTabWidget() self.layout().addWidget(mainWidget) self.setWindowTitle(i18n("Comic Metadata")) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.layout().addWidget(buttons) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) # Title, concept, summary, genre, characters, format, rating, language, series, other keywords metadataPage = QWidget() mformLayout = QFormLayout() metadataPage.setLayout(mformLayout) self.lnTitle = QLineEdit() self.lnTitle.setToolTip(i18n("The proper title of the comic.")) self.teSummary = QPlainTextEdit() self.teSummary.setToolTip(i18n("What will you tell others to entice them to read your comic?")) self.lnGenre = QLineEdit() genreCompletion = multi_entry_completer() genreCompletion.setModel(QStringListModel(self.genreKeysList)) self.lnGenre.setCompleter(genreCompletion) genreCompletion.setCaseSensitivity(False) self.lnGenre.setToolTip(i18n("The genre of the work. Prefilled values are from the ACBF, but you can fill in your own. Separate genres with commas. Try to limit the amount to about two or three.")) self.lnCharacters = QLineEdit() characterCompletion = multi_entry_completer() characterCompletion.setModel(QStringListModel(self.characterKeysList)) characterCompletion.setCaseSensitivity(False) characterCompletion.setFilterMode(Qt.MatchContains) # So that if there is a list of names with last names, people can type in a last name. self.lnCharacters.setCompleter(characterCompletion) self.lnCharacters.setToolTip(i18n("The names of the characters that this comic revolves around. Comma-separated.")) self.lnFormat = QLineEdit() formatCompletion = multi_entry_completer() formatCompletion.setModel(QStringListModel(self.formatKeysList)) formatCompletion.setCaseSensitivity(False) self.lnFormat.setCompleter(formatCompletion) ratingLayout = QHBoxLayout() self.cmbRatingSystem = QComboBox() self.cmbRatingSystem.addItems(self.ratingKeysList.keys()) self.cmbRatingSystem.setEditable(True) self.cmbRating = QComboBox() self.cmbRating.setEditable(True) self.cmbRatingSystem.currentIndexChanged.connect(self.slot_refill_ratings) ratingLayout.addWidget(self.cmbRatingSystem) ratingLayout.addWidget(self.cmbRating) self.lnSeriesName = QLineEdit() self.lnSeriesName.setToolTip(i18n("If this is part of a series, enter the name of the series and the number.")) self.spnSeriesNumber = QSpinBox() self.spnSeriesNumber.setPrefix(i18n("No. ")) self.spnSeriesVol = QSpinBox() self.spnSeriesVol.setPrefix(i18n("Vol. ")) seriesLayout = QHBoxLayout() seriesLayout.addWidget(self.lnSeriesName) seriesLayout.addWidget(self.spnSeriesVol) seriesLayout.addWidget(self.spnSeriesNumber) otherCompletion = multi_entry_completer() otherCompletion.setModel(QStringListModel(self.otherKeysList)) otherCompletion.setCaseSensitivity(False) otherCompletion.setFilterMode(Qt.MatchContains) self.lnOtherKeywords = QLineEdit() self.lnOtherKeywords.setCompleter(otherCompletion) self.lnOtherKeywords.setToolTip(i18n("Other keywords that do not fit in the previously mentioned sets. As always, comma-separated.")) self.cmbLanguage = language_combo_box() self.cmbCountry = country_combo_box() self.cmbLanguage.currentIndexChanged.connect(self.slot_update_countries) self.cmbReadingMode = QComboBox() self.cmbReadingMode.addItem(i18n("Left to Right")) self.cmbReadingMode.addItem(i18n("Right to Left")) self.cmbCoverPage = QComboBox() self.cmbCoverPage.setToolTip(i18n("Which page is the cover page? This will be empty if there are no pages.")) mformLayout.addRow(i18n("Title:"), self.lnTitle) mformLayout.addRow(i18n("Cover page:"), self.cmbCoverPage) mformLayout.addRow(i18n("Summary:"), self.teSummary) mformLayout.addRow(i18n("Language:"), self.cmbLanguage) mformLayout.addRow("", self.cmbCountry) mformLayout.addRow(i18n("Reading direction:"), self.cmbReadingMode) mformLayout.addRow(i18n("Genre:"), self.lnGenre) mformLayout.addRow(i18n("Characters:"), self.lnCharacters) mformLayout.addRow(i18n("Format:"), self.lnFormat) mformLayout.addRow(i18n("Rating:"), ratingLayout) mformLayout.addRow(i18n("Series:"), seriesLayout) mformLayout.addRow(i18n("Other:"), self.lnOtherKeywords) mainWidget.addTab(metadataPage, i18n("Work")) # The page for the authors. authorPage = QWidget() authorPage.setLayout(QVBoxLayout()) explanation = QLabel(i18n("The following is a table of the authors that contributed to this comic. You can set their nickname, proper names (first, middle, last), role (penciller, inker, etc), email and homepage.")) explanation.setWordWrap(True) self.authorModel = QStandardItemModel(0, 8) labels = [i18n("Nick Name"), i18n("Given Name"), i18n("Middle Name"), i18n("Family Name"), i18n("Role"), i18n("Email"), i18n("Homepage"), i18n("Language")] self.authorModel.setHorizontalHeaderLabels(labels) self.authorTable = QTableView() self.authorTable.setModel(self.authorModel) self.authorTable.verticalHeader().setDragEnabled(True) self.authorTable.verticalHeader().setDropIndicatorShown(True) self.authorTable.verticalHeader().setSectionsMovable(True) self.authorTable.verticalHeader().sectionMoved.connect(self.slot_reset_author_row_visual) delegate = author_delegate() delegate.setCompleterData(self.authorRoleList, 4) delegate.setLanguageData(len(labels) - 1) self.authorTable.setItemDelegate(delegate) author_button_layout = QWidget() author_button_layout.setLayout(QHBoxLayout()) btn_add_author = QPushButton(i18n("Add Author")) btn_add_author.clicked.connect(self.slot_add_author) btn_remove_author = QPushButton(i18n("Remove Author")) btn_remove_author.clicked.connect(self.slot_remove_author) author_button_layout.layout().addWidget(btn_add_author) author_button_layout.layout().addWidget(btn_remove_author) authorPage.layout().addWidget(explanation) authorPage.layout().addWidget(self.authorTable) authorPage.layout().addWidget(author_button_layout) mainWidget.addTab(authorPage, i18n("Authors")) # The page with publisher information. publisherPage = QWidget() publisherLayout = QFormLayout() publisherPage.setLayout(publisherLayout) self.publisherName = QLineEdit() self.publisherName.setToolTip(i18n("The name of the company, group or person who is responsible for the final version the reader gets.")) publishDateLayout = QHBoxLayout() self.publishDate = QDateEdit() self.publishDate.setDisplayFormat(QLocale().system().dateFormat()) currentDate = QPushButton(i18n("Set Today")) currentDate.setToolTip(i18n("Sets the publish date to the current date.")) currentDate.clicked.connect(self.slot_set_date) publishDateLayout.addWidget(self.publishDate) publishDateLayout.addWidget(currentDate) self.publishCity = QLineEdit() self.publishCity.setToolTip(i18n("Traditional publishers are always mentioned in source with the city they are located.")) self.isbn = QLineEdit() self.license = license_combo_box() # Maybe ought to make this a QLineEdit... self.license.setEditable(True) self.license.completer().setCompletionMode(QCompleter.PopupCompletion) dataBaseReference = QVBoxLayout() self.ln_database_name = QLineEdit() self.ln_database_name.setToolTip(i18n("If there is an entry in a comics data base, that should be added here. It is unlikely to be a factor for comics from scratch, but useful when doing a conversion.")) self.cmb_entry_type = QComboBox() self.cmb_entry_type.addItems(["IssueID", "SeriesID", "URL"]) self.cmb_entry_type.setEditable(True) self.ln_source = QLineEdit() - self.ln_source.setToolTip(i18n("Whether the comic is an adaption of an existing source, and if so, how to find information about that source. So for example, for an adapted webcomic, the official website url should go here.")) + self.ln_source.setToolTip(i18n("Whether the comic is an adaptation of an existing source, and if so, how to find information about that source. So for example, for an adapted webcomic, the official website url should go here.")) self.label_uuid = QLabel() self.label_uuid.setToolTip(i18n("By default this will be filled with a generated universal unique identifier. The ID by itself is merely so that comic book library management programs can figure out if this particular comic is already in their database and whether it has been rated. Of course, the UUID can be changed into something else by manually changing the JSON, but this is advanced usage.")) self.ln_database_entry = QLineEdit() dbHorizontal = QHBoxLayout() dbHorizontal.addWidget(self.ln_database_name) dbHorizontal.addWidget(self.cmb_entry_type) dataBaseReference.addLayout(dbHorizontal) dataBaseReference.addWidget(self.ln_database_entry) publisherLayout.addRow(i18n("Name:"), self.publisherName) publisherLayout.addRow(i18n("City:"), self.publishCity) publisherLayout.addRow(i18n("Date:"), publishDateLayout) publisherLayout.addRow(i18n("ISBN:"), self.isbn) publisherLayout.addRow(i18n("Source:"), self.ln_source) publisherLayout.addRow(i18n("UUID:"), self.label_uuid) publisherLayout.addRow(i18n("License:"), self.license) publisherLayout.addRow(i18n("Database:"), dataBaseReference) mainWidget.addTab(publisherPage, i18n("Publisher")) """ Ensure that the drag and drop of authors doesn't mess up the labels. """ def slot_reset_author_row_visual(self): headerLabelList = [] for i in range(self.authorTable.verticalHeader().count()): headerLabelList.append(str(i)) for i in range(self.authorTable.verticalHeader().count()): logicalI = self.authorTable.verticalHeader().logicalIndex(i) headerLabelList[logicalI] = str(i + 1) self.authorModel.setVerticalHeaderLabels(headerLabelList) """ Set the publish date to the current date. """ def slot_set_date(self): self.publishDate.setDate(QDate().currentDate()) def slot_update_countries(self): code = self.cmbLanguage.codeForCurrentEntry() self.cmbCountry.set_country_for_locale(code) """ Append keys to autocompletion lists from the directory mainP. """ def get_auto_completion_keys(self, mainP=Path()): genre = Path(mainP / "key_genre") characters = Path(mainP / "key_characters") rating = Path(mainP / "key_rating") format = Path(mainP / "key_format") keywords = Path(mainP / "key_other") authorRole = Path(mainP / "key_author_roles") if genre.exists(): for t in list(genre.glob('**/*.txt')): file = open(str(t), "r", errors="replace") for l in file: if str(l).strip("\n") not in self.genreKeysList: self.genreKeysList.append(str(l).strip("\n")) file.close() if characters.exists(): for t in list(characters.glob('**/*.txt')): file = open(str(t), "r", errors="replace") for l in file: if str(l).strip("\n") not in self.characterKeysList: self.characterKeysList.append(str(l).strip("\n")) file.close() if format.exists(): for t in list(format.glob('**/*.txt')): file = open(str(t), "r", errors="replace") for l in file: if str(l).strip("\n") not in self.formatKeysList: self.formatKeysList.append(str(l).strip("\n")) file.close() if rating.exists(): for t in list(rating.glob('**/*.csv')): file = open(str(t), "r", newline="", encoding="utf-8") ratings = csv.reader(file) title = os.path.basename(str(t)) r = 0 for row in ratings: listItem = [] if r is 0: title = row[1] else: listItem = self.ratingKeysList[title] item = [] item.append(row[0]) item.append(row[1]) listItem.append(item) self.ratingKeysList[title] = listItem r += 1 file.close() if keywords.exists(): for t in list(keywords.glob('**/*.txt')): file = open(str(t), "r", errors="replace") for l in file: if str(l).strip("\n") not in self.otherKeysList: self.otherKeysList.append(str(l).strip("\n")) file.close() if authorRole.exists(): for t in list(authorRole.glob('**/*.txt')): file = open(str(t), "r", errors="replace") for l in file: if str(l).strip("\n") not in self.authorRoleList: self.authorRoleList.append(str(l).strip("\n")) file.close() """ Refill the ratings box. This is called whenever the rating system changes. """ def slot_refill_ratings(self): if self.cmbRatingSystem.currentText() in self.ratingKeysList.keys(): self.cmbRating.clear() model = QStandardItemModel() for i in self.ratingKeysList[self.cmbRatingSystem.currentText()]: item = QStandardItem() item.setText(i[0]) item.setToolTip(i[1]) model.appendRow(item) self.cmbRating.setModel(model) """ Add an author with default values initialised. """ def slot_add_author(self): listItems = [] listItems.append(QStandardItem(i18n("Anon"))) # Nick name listItems.append(QStandardItem(i18n("John"))) # First name listItems.append(QStandardItem()) # Middle name listItems.append(QStandardItem(i18n("Doe"))) # Last name listItems.append(QStandardItem()) # role listItems.append(QStandardItem()) # email listItems.append(QStandardItem()) # homepage language = QLocale.system().name().split("_")[0] if language == "C": language = "en" listItems.append(QStandardItem(language)) # Language self.authorModel.appendRow(listItems) """ Remove the selected author from the author list. """ def slot_remove_author(self): self.authorModel.removeRow(self.authorTable.currentIndex().row()) """ Load the UI values from the config dictionary given. """ def setConfig(self, config): if "title" in config.keys(): self.lnTitle.setText(config["title"]) self.teSummary.clear() if "pages" in config.keys(): self.cmbCoverPage.clear() for page in config["pages"]: self.cmbCoverPage.addItem(page) if "cover" in config.keys(): if config["cover"] in config["pages"]: self.cmbCoverPage.setCurrentText(config["cover"]) if "summary" in config.keys(): self.teSummary.appendPlainText(config["summary"]) if "genre" in config.keys(): genreList = [] genreListConf = config["genre"] totalMatch = 100 if isinstance(config["genre"], dict): genreListConf = config["genre"].keys() totalMatch = 0 for genre in genreListConf: genreKey = genre if genre in self.acbfGenreList: genreKey = self.acbfGenreList[genre] if isinstance(config["genre"], dict): genreValue = config["genre"][genre] if genreValue > 0: genreKey = str(genreKey + "(" + str(genreValue) + ")") genreList.append(genreKey) self.lnGenre.setText(", ".join(genreList)) if "characters" in config.keys(): self.lnCharacters.setText(", ".join(config["characters"])) if "format" in config.keys(): self.lnFormat.setText(", ".join(config["format"])) if "rating" in config.keys(): self.cmbRating.setCurrentText(config["rating"]) else: self.cmbRating.setCurrentText("") if "ratingSystem" in config.keys(): self.cmbRatingSystem.setCurrentText(config["ratingSystem"]) else: self.cmbRatingSystem.setCurrentText("") if "otherKeywords" in config.keys(): self.lnOtherKeywords.setText(", ".join(config["otherKeywords"])) if "seriesName" in config.keys(): self.lnSeriesName.setText(config["seriesName"]) if "seriesVolume" in config.keys(): self.spnSeriesVol.setValue(config["seriesVolume"]) if "seriesNumber" in config.keys(): self.spnSeriesNumber.setValue(config["seriesNumber"]) if "language" in config.keys(): code = config["language"] if "_" in code: self.cmbLanguage.setEntryToCode(code.split("_")[0]) self.cmbCountry.setEntryToCode(code.split("_")[-1]) elif "-" in code: self.cmbLanguage.setEntryToCode(code.split("-")[0]) self.cmbCountry.setEntryToCode(code.split("-")[-1]) else: self.cmbLanguage.setEntryToCode(code) if "readingDirection" in config.keys(): if config["readingDirection"] is "leftToRight": self.cmbReadingMode.setCurrentIndex(int(Qt.LeftToRight)) else: self.cmbReadingMode.setCurrentIndex(int(Qt.RightToLeft)) else: self.cmbReadingMode.setCurrentIndex(QLocale(self.cmbLanguage.codeForCurrentEntry()).textDirection()) if "publisherName" in config.keys(): self.publisherName.setText(config["publisherName"]) if "publisherCity" in config.keys(): self.publishCity.setText(config["publisherCity"]) if "publishingDate" in config.keys(): self.publishDate.setDate(QDate.fromString(config["publishingDate"], Qt.ISODate)) if "isbn-number" in config.keys(): self.isbn.setText(config["isbn-number"]) if "source" in config.keys(): self.ln_source.setText(config["source"]) elif "acbfSource" in config.keys(): self.ln_source.setText(config["acbfSource"]) if "uuid" in config.keys(): self.label_uuid.setText(config["uuid"]) else: uuid = str() if "acbfID" in config.keys(): uuid = config["acbfID"] uuid = uuid.strip("{") uuid = uuid.strip("}") uuidVerify = uuid.split("-") if len(uuidVerify[0])!=8 or len(uuidVerify[1])!=4 or len(uuidVerify[2])!=4 or len(uuidVerify[3])!=4 or len(uuidVerify[4])!=12: uuid = QUuid.createUuid().toString() self.label_uuid.setText(uuid) config["uuid"] = uuid if "license" in config.keys(): self.license.setCurrentText(config["license"]) else: self.license.setCurrentText("") # I would like to keep it ambiguous whether the artist has thought about the license or not. if "authorList" in config.keys(): authorList = config["authorList"] for i in range(len(authorList)): author = authorList[i] if len(author.keys()) > 0: listItems = [] listItems = [] listItems.append(QStandardItem(author.get("nickname", ""))) listItems.append(QStandardItem(author.get("first-name", ""))) listItems.append(QStandardItem(author.get("initials", ""))) listItems.append(QStandardItem(author.get("last-name", ""))) role = author.get("role", "") if role in self.acbfAuthorRolesList.keys(): role = self.acbfAuthorRolesList[role] listItems.append(QStandardItem(role)) listItems.append(QStandardItem(author.get("email", ""))) listItems.append(QStandardItem(author.get("homepage", ""))) listItems.append(QStandardItem(author.get("language", ""))) self.authorModel.appendRow(listItems) else: self.slot_add_author() dbRef = config.get("databaseReference", {}) self.ln_database_name.setText(dbRef.get("name", "")) self.ln_database_entry.setText(dbRef.get("entry", "")) stringCmbEntryType = self.cmb_entry_type.itemText(0) self.cmb_entry_type.setCurrentText(dbRef.get("type", stringCmbEntryType)) """ Store the GUI values into the config dictionary given. @return the config diactionary filled with new values. """ def getConfig(self, config): text = self.lnTitle.text() if len(text) > 0 and text.isspace() is False: config["title"] = text elif "title" in config.keys(): config.pop("title") config["cover"] = self.cmbCoverPage.currentText() listkeys = self.lnGenre.text() if len(listkeys) > 0 and listkeys.isspace() is False: preSplit = self.lnGenre.text().split(",") genreMatcher = re.compile(r'\((\d+)\)') genreList = {} totalValue = 0 for key in preSplit: m = genreMatcher.search(key) if m: genre = str(genreMatcher.sub("", key)).strip() match = int(m.group()[:-1][1:]) else: genre = key.strip() match = 0 if genre in self.acbfGenreList.values(): i = list(self.acbfGenreList.values()).index(genre) genreList[list(self.acbfGenreList.keys())[i]] = match else: genreList[genre] = match totalValue += match # Normalize the values: for key in genreList.keys(): if genreList[key] > 0: genreList[key] = round(genreList[key] / totalValue * 100) config["genre"] = genreList elif "genre" in config.keys(): config.pop("genre") listkeys = self.lnCharacters.text() if len(listkeys) > 0 and listkeys.isspace() is False: config["characters"] = self.lnCharacters.text().split(", ") elif "characters" in config.keys(): config.pop("characters") listkeys = self.lnFormat.text() if len(listkeys) > 0 and listkeys.isspace() is False: config["format"] = self.lnFormat.text().split(", ") elif "format" in config.keys(): config.pop("format") config["ratingSystem"] = self.cmbRatingSystem.currentText() config["rating"] = self.cmbRating.currentText() listkeys = self.lnOtherKeywords.text() if len(listkeys) > 0 and listkeys.isspace() is False: config["otherKeywords"] = self.lnOtherKeywords.text().split(", ") elif "otherKeywords" in config.keys(): config.pop("otherKeywords") text = self.teSummary.toPlainText() if len(text) > 0 and text.isspace() is False: config["summary"] = text elif "summary" in config.keys(): config.pop("summary") if len(self.lnSeriesName.text()) > 0: config["seriesName"] = self.lnSeriesName.text() config["seriesNumber"] = self.spnSeriesNumber.value() if self.spnSeriesVol.value() > 0: config["seriesVolume"] = self.spnSeriesVol.value() config["language"] = str(self.cmbLanguage.codeForCurrentEntry()+"-"+self.cmbCountry.codeForCurrentEntry()) if self.cmbReadingMode.currentIndex() is int(Qt.LeftToRight): config["readingDirection"] = "leftToRight" else: config["readingDirection"] = "rightToLeft" authorList = [] for row in range(self.authorTable.verticalHeader().count()): logicalIndex = self.authorTable.verticalHeader().logicalIndex(row) listEntries = ["nickname", "first-name", "initials", "last-name", "role", "email", "homepage", "language"] author = {} for i in range(len(listEntries)): entry = self.authorModel.data(self.authorModel.index(logicalIndex, i)) if entry is None: entry = " " if entry.isspace() is False and len(entry) > 0: if listEntries[i] == "role": if entry in self.acbfAuthorRolesList.values(): entryI = list(self.acbfAuthorRolesList.values()).index(entry) entry = list(self.acbfAuthorRolesList.keys())[entryI] author[listEntries[i]] = entry elif listEntries[i] in author.keys(): author.pop(listEntries[i]) authorList.append(author) config["authorList"] = authorList config["publisherName"] = self.publisherName.text() config["publisherCity"] = self.publishCity.text() config["publishingDate"] = self.publishDate.date().toString(Qt.ISODate) config["isbn-number"] = self.isbn.text() config["source"] = self.ln_source.text() config["license"] = self.license.currentText() if self.ln_database_name.text().isalnum() and self.ln_database_entry.text().isalnum(): dbRef = {} dbRef["name"] = self.ln_database_name.text() dbRef["entry"] = self.ln_database_entry.text() dbRef["type"] = self.cmb_entry_type.currentText() config["databaseReference"] = dbRef return config diff --git a/plugins/tools/karbonplugins/tools/CalligraphyTool/tutorial/TUTORIAL b/plugins/tools/karbonplugins/tools/CalligraphyTool/tutorial/TUTORIAL index 98585475e1..8138f3e076 100644 --- a/plugins/tools/karbonplugins/tools/CalligraphyTool/tutorial/TUTORIAL +++ b/plugins/tools/karbonplugins/tools/CalligraphyTool/tutorial/TUTORIAL @@ -1,108 +1,108 @@ Adapted from the Inkscape tutorial by: Copyright (C) 2004-2006 Bulia Byak Copyright (C) 2004-2006 Josh Andler Adaptation to Karbon by: Copyright (C) 2008 Fela Winkelmolen This tutorial 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. One of the many great tools available in Karbon is the Calligraphy tool. This tutorial will help you become acquainted with how that tool works, as well as demonstrate some basic techniques of the art of Calligraphy. History and Styles ================== Going by the dictionary definition, calligraphy means "beautiful writing" or "fair or elegant penmanship". Essentially, calligraphy is the art of making beautiful or elegant handwriting. It may sound intimidating, but with a little practice, anyone can master the basics of this art. The earliest forms of calligraphy date back to cave-man paintings. Up until roughly 1440 AD, before the printing press was around, calligraphy was the way books and other publications were made. A scribe had to handwrite every individual copy of every book or publication. The handwriting was done with a quill and ink onto materials such as parchment or vellum. The lettering styles used throughout the ages include Rustic, Carolingian, Blackletter, etc. Perhaps the most common place where the average person will run across calligraphy today is on wedding invitations. There are three main styles of calligraphy: * Western or Roman * Arabic * Chinese or Oriental This tutorial focuses mainly on Western calligraphy, as the other two styles tend to use a brush (instead of a pen with nib), which is not how our Calligraphy tool currently functions. One great advantage that we have over the scribes of the past is the Undo command: If you make a mistake, the entire page is not ruined. Hardware ======== You'll get the best results if you use a tablet and pen (e.g. Wacom). However, even with mouse you can do some beginning calligraphy, though you will have difficulty producing fast sweeping strokes. Calligraphy Tool Options ======================== Switch to the Calligraphy tool by clicking on its button in the tools docker. In the "Tool options" docker you can set several parameters, most of those are shown only after pressing "show details >>". The following sections will explain their use. Following an Existing Path ========================== When checking the "Follow selected path" option the stroke will follow the outline of the currently selected path, instead of following the mouse or tablet position. This way you can use the calligraphy tool to draw poligons, stars, etc. Or you can fist use the path tool to create a precise guide path, and than use the calligraphy tool on the created path. Width & Thinning ================ This pair of options control the width of your pen. Since pen width is changed often, you can adjust it without going to the docker, using the left and right arrow keys. The best thing about these keys is that they work while you're drawing, so you can change the width of your pen gradually in the middle of the stroke. - + Pen width may also depend on the velocity, as controlled by the thinning parameter. This parameter can take values from -1 to 1; zero means the width is independent of velocity, positive values make faster strokes thinner, negative values make faster strokes broader. Here are a few examples, the two left strokes having positive thinning, and the right two negative thinning, all strokes are drawn with the same width. For fun, set the thinning to 1 (maximum) and draw with jerky movements to get strangely naturalistic, neuron-like shapes: Angle & Fixation ================ After width, angle is the most important calligraphy parameter. It is the angle of your pen in degrees, changing from 0 (horizontal) to 179 passing through 90 (vertical): Each traditional calligraphy style has its own prevalent pen angle. For example, the Unicial hand uses the angle of 25 degrees. More complex hands and more experienced calligraphers will often vary the angle while drawing, and Karbon makes this possible by pressing up and down arrow keys. For beginning calligraphy lessons, however, keeping the angle constant will work best. Here are examples of strokes drawn at different angles (fixation = 1): As you can see, the stroke is at its thinnest when it is drawn parallel to its angle, and at its broadest when drawn perpendicular. Positive angles are the most natural and traditional for right-handed calligraphy. The level of contrast between the thinnest and the thickest is controlled by the fixation parameter. The value of 1 means that the angle is always constant, as set in the Angle field. Decreasing fixation lets the pen turn a little against the direction of the stroke. With fixation=0, pen rotates freely to be always perpendicular to the stroke, and Angle has no effect anymore: Typographically speaking, maximum fixation and therefore maximum stroke width contrast (above left) are the features of antique serif typefaces, such as Times or Bodoni (because these typefaces were historically an imitation of fixed-pen calligraphy). Zero fixation and zero width contrast (above right), on the other hand, suggest modern sans serif typefaces such as Helvetica. Mass & Drag =========== Unlike width and angle, these two last parameters define how the tool "feels" rather than affect its visual output. So there won't be any illustrations in this section; instead just try them yourself to get a better idea. In physics, mass is what causes inertia; the higher the mass of the Karbon calligraphy tool, the more it lags behind your mouse pointer and the more it smoothes out sharp turns and quick jerks in your stroke. When this value is small the tool is fast and responsive, but you increase the mass to get slower and smoother pen. Drag is the resistance of the paper to the movement of the pen. Lowering this parameter makes paper "slippery": if the mass is big, the pen tends to run away on sharp turns; if the mass is zero, low drag makes the pen to wiggle wildly. Calligraphy examples ==================== Now that you know the basic capabilities of the tool, you can try to produce some real calligraphy. If you are new to this art, get yourself a good calligraphy book and study it with Inkscape. This section will show you just a few simple examples. First of all, to do letters, you need to create a pair of rulers to guide you. If you're going to write in a slanted or cursive hand, add some slanted guides across the two rulers as well, for example: Then zoom in so that the height between the rulers corresponds to your most natural hand movement range, adjust width and angle, and off you go! Probably the first thing you would do as a beginner calligrapher is practice the basic elements of letters -- vertical and horizontal stems, round strokes, slanted stems. Here are some letter elements for the Unicial hand: Some useful tips: * Double clicking on a calligraphic shape will allow you to edit the control points of the guide path. For further finetuning you can convert the calligraphic shape into a simple path clicking on the "Convert To Path" button. * If your last stroke is bad, just undo it (Ctrl+Z). However, if its shape is good but the position or size are slightly off, it's better to switch to the default tool temporarily and nudge/scale/rotate it as needed, then return to the calligraphy tool. * Having done a word, switch to the default tool again to adjust stem uniformity and letterspacing. Don't overdo this, however; good calligraphy must retain somewhat irregular handwritten look. Resist the temptation to copy over letters and letter elements; each stroke must be original. And here are some complete lettering examples (taken from the original Inkscape tutorial as a lot of the other images and text): Profiles ======== Karbon allows you to easily load and save predefined and custom sets of options, called profiles. The combo box in the top of the "Tool options" docker allows you to choose the profile. To save a profile just click on the "Save profile as..." button (if the details are hidden first press "Show details >>") after which you will be asked to enter a name for the new profile. Conclusion ========== Calligraphy is not only fun; it's a deeply spiritual art that may transform your outlook on everything you do and see. Inkscape's calligraphy tool can only serve as a modest introduction. And yet it is very nice to play with and may be useful in real design. Enjoy! \ No newline at end of file