Add Material 3 candidate window addon for Fcitx5
Fork of the classicui addon (Fcitx5 5.1.23) with a Material 3 look, spring based animations and a right click action menu for candidates. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
No files matched your search
@@ -0,0 +1,3 @@
|
||||
build/
|
||||
.cache/
|
||||
*.swp
|
||||
@@ -0,0 +1,468 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
|
||||
Version 2.1, February 1999
|
||||
|
||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this license
|
||||
document, but changing it is not allowed.
|
||||
|
||||
[This is the first released version of the Lesser GPL. It also counts as the
|
||||
successor of the GNU Library Public License, version 2, hence the version
|
||||
number 2.1.]
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your freedom to share
|
||||
and change it. By contrast, the GNU General Public Licenses are intended to
|
||||
guarantee your freedom to share and change free software--to make sure the
|
||||
software is free for all its users.
|
||||
|
||||
This license, the Lesser General Public License, applies to some specially
|
||||
designated software packages--typically libraries--of the Free Software Foundation
|
||||
and other authors who decide to use it. You can use it too, but we suggest
|
||||
you first think carefully about whether this license or the ordinary General
|
||||
Public License is the better strategy to use in any particular case, based
|
||||
on the explanations below.
|
||||
|
||||
When we speak of free software, we are referring to freedom of use, not price.
|
||||
Our General Public Licenses are designed to make sure that you have the freedom
|
||||
to distribute copies of free software (and charge for this service if you
|
||||
wish); that you receive source code or can get it if you want it; that you
|
||||
can change the software and use pieces of it in new free programs; and that
|
||||
you are informed that you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid distributors
|
||||
to deny you these rights or to ask you to surrender these rights. These restrictions
|
||||
translate to certain responsibilities for you if you distribute copies of
|
||||
the library or if you modify it.
|
||||
|
||||
For example, if you distribute copies of the library, whether gratis or for
|
||||
a fee, you must give the recipients all the rights that we gave you. You must
|
||||
make sure that they, too, receive or can get the source code. If you link
|
||||
other code with the library, you must provide complete object files to the
|
||||
recipients, so that they can relink them with the library after making changes
|
||||
to the library and recompiling it. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
We protect your rights with a two-step method: (1) we copyright the library,
|
||||
and (2) we offer you this license, which gives you legal permission to copy,
|
||||
distribute and/or modify the library.
|
||||
|
||||
To protect each distributor, we want to make it very clear that there is no
|
||||
warranty for the free library. Also, if the library is modified by someone
|
||||
else and passed on, the recipients should know that what they have is not
|
||||
the original version, so that the original author's reputation will not be
|
||||
affected by problems that might be introduced by others.
|
||||
|
||||
Finally, software patents pose a constant threat to the existence of any free
|
||||
program. We wish to make sure that a company cannot effectively restrict the
|
||||
users of a free program by obtaining a restrictive license from a patent holder.
|
||||
Therefore, we insist that any patent license obtained for a version of the
|
||||
library must be consistent with the full freedom of use specified in this
|
||||
license.
|
||||
|
||||
Most GNU software, including some libraries, is covered by the ordinary GNU
|
||||
General Public License. This license, the GNU Lesser General Public License,
|
||||
applies to certain designated libraries, and is quite different from the ordinary
|
||||
General Public License. We use this license for certain libraries in order
|
||||
to permit linking those libraries into non-free programs.
|
||||
|
||||
When a program is linked with a library, whether statically or using a shared
|
||||
library, the combination of the two is legally speaking a combined work, a
|
||||
derivative of the original library. The ordinary General Public License therefore
|
||||
permits such linking only if the entire combination fits its criteria of freedom.
|
||||
The Lesser General Public License permits more lax criteria for linking other
|
||||
code with the library.
|
||||
|
||||
We call this license the "Lesser" General Public License because it does Less
|
||||
to protect the user's freedom than the ordinary General Public License. It
|
||||
also provides other free software developers Less of an advantage over competing
|
||||
non-free programs. These disadvantages are the reason we use the ordinary
|
||||
General Public License for many libraries. However, the Lesser license provides
|
||||
advantages in certain special circumstances.
|
||||
|
||||
For example, on rare occasions, there may be a special need to encourage the
|
||||
widest possible use of a certain library, so that it becomes a de-facto standard.
|
||||
To achieve this, non-free programs must be allowed to use the library. A more
|
||||
frequent case is that a free library does the same job as widely used non-free
|
||||
libraries. In this case, there is little to gain by limiting the free library
|
||||
to free software only, so we use the Lesser General Public License.
|
||||
|
||||
In other cases, permission to use a particular library in non-free programs
|
||||
enables a greater number of people to use a large body of free software. For
|
||||
example, permission to use the GNU C Library in non-free programs enables
|
||||
many more people to use the whole GNU operating system, as well as its variant,
|
||||
the GNU/Linux operating system.
|
||||
|
||||
Although the Lesser General Public License is Less protective of the users'
|
||||
freedom, it does ensure that the user of a program that is linked with the
|
||||
Library has the freedom and the wherewithal to run that program using a modified
|
||||
version of the Library.
|
||||
|
||||
The precise terms and conditions for copying, distribution and modification
|
||||
follow. Pay close attention to the difference between a "work based on the
|
||||
library" and a "work that uses the library". The former contains code derived
|
||||
from the library, whereas the latter must be combined with the library in
|
||||
order to run.
|
||||
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License Agreement applies to any software library or other program
|
||||
which contains a notice placed by the copyright holder or other authorized
|
||||
party saying it may be distributed under the terms of this Lesser General
|
||||
Public License (also called "this License"). Each licensee is addressed as
|
||||
"you".
|
||||
|
||||
A "library" means a collection of software functions and/or data prepared
|
||||
so as to be conveniently linked with application programs (which use some
|
||||
of those functions and data) to form executables.
|
||||
|
||||
The "Library", below, refers to any such software library or work which has
|
||||
been distributed under these terms. A "work based on the Library" means either
|
||||
the Library or any derivative work under copyright law: that is to say, a
|
||||
work containing the Library or a portion of it, either verbatim or with modifications
|
||||
and/or translated straightforwardly into another language. (Hereinafter, translation
|
||||
is included without limitation in the term "modification".)
|
||||
|
||||
"Source code" for a work means the preferred form of the work for making modifications
|
||||
to it. For a library, complete source code means all the source code for all
|
||||
modules it contains, plus any associated interface definition files, plus
|
||||
the scripts used to control compilation and installation of the library.
|
||||
|
||||
Activities other than copying, distribution and modification are not covered
|
||||
by this License; they are outside its scope. The act of running a program
|
||||
using the Library is not restricted, and output from such a program is covered
|
||||
only if its contents constitute a work based on the Library (independent of
|
||||
the use of the Library in a tool for writing it). Whether that is true depends
|
||||
on what the Library does and what the program that uses the Library does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Library's complete source
|
||||
code as you receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice and disclaimer
|
||||
of warranty; keep intact all the notices that refer to this License and to
|
||||
the absence of any warranty; and distribute a copy of this License along with
|
||||
the Library.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and you
|
||||
may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Library or any portion of it,
|
||||
thus forming a work based on the Library, and copy and distribute such modifications
|
||||
or work under the terms of Section 1 above, provided that you also meet all
|
||||
of these conditions:
|
||||
|
||||
a) The modified work must itself be a software library.
|
||||
|
||||
b) You must cause the files modified to carry prominent notices stating that
|
||||
you changed the files and the date of any change.
|
||||
|
||||
c) You must cause the whole of the work to be licensed at no charge to all
|
||||
third parties under the terms of this License.
|
||||
|
||||
d) If a facility in the modified Library refers to a function or a table of
|
||||
data to be supplied by an application program that uses the facility, other
|
||||
than as an argument passed when the facility is invoked, then you must make
|
||||
a good faith effort to ensure that, in the event an application does not supply
|
||||
such function or table, the facility still operates, and performs whatever
|
||||
part of its purpose remains meaningful.
|
||||
|
||||
(For example, a function in a library to compute square roots has a purpose
|
||||
that is entirely well-defined independent of the application. Therefore, Subsection
|
||||
2d requires that any application-supplied function or table used by this function
|
||||
must be optional: if the application does not supply it, the square root function
|
||||
must still compute square roots.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If identifiable
|
||||
sections of that work are not derived from the Library, and can be reasonably
|
||||
considered independent and separate works in themselves, then this License,
|
||||
and its terms, do not apply to those sections when you distribute them as
|
||||
separate works. But when you distribute the same sections as part of a whole
|
||||
which is a work based on the Library, the distribution of the whole must be
|
||||
on the terms of this License, whose permissions for other licensees extend
|
||||
to the entire whole, and thus to each and every part regardless of who wrote
|
||||
it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest your
|
||||
rights to work written entirely by you; rather, the intent is to exercise
|
||||
the right to control the distribution of derivative or collective works based
|
||||
on the Library.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Library with
|
||||
the Library (or with a work based on the Library) on a volume of a storage
|
||||
or distribution medium does not bring the other work under the scope of this
|
||||
License.
|
||||
|
||||
3. You may opt to apply the terms of the ordinary GNU General Public License
|
||||
instead of this License to a given copy of the Library. To do this, you must
|
||||
alter all the notices that refer to this License, so that they refer to the
|
||||
ordinary GNU General Public License, version 2, instead of to this License.
|
||||
(If a newer version than version 2 of the ordinary GNU General Public License
|
||||
has appeared, then you can specify that version instead if you wish.) Do not
|
||||
make any other change in these notices.
|
||||
|
||||
Once this change is made in a given copy, it is irreversible for that copy,
|
||||
so the ordinary GNU General Public License applies to all subsequent copies
|
||||
and derivative works made from that copy.
|
||||
|
||||
This option is useful when you wish to copy part of the code of the Library
|
||||
into a program that is not a library.
|
||||
|
||||
4. You may copy and distribute the Library (or a portion or derivative of
|
||||
it, under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you accompany it with the complete corresponding
|
||||
machine-readable source code, which must be distributed under the terms of
|
||||
Sections 1 and 2 above on a medium customarily used for software interchange.
|
||||
|
||||
If distribution of object code is made by offering access to copy from a designated
|
||||
place, then offering equivalent access to copy the source code from the same
|
||||
place satisfies the requirement to distribute the source code, even though
|
||||
third parties are not compelled to copy the source along with the object code.
|
||||
|
||||
5. A program that contains no derivative of any portion of the Library, but
|
||||
is designed to work with the Library by being compiled or linked with it,
|
||||
is called a "work that uses the Library". Such a work, in isolation, is not
|
||||
a derivative work of the Library, and therefore falls outside the scope of
|
||||
this License.
|
||||
|
||||
However, linking a "work that uses the Library" with the Library creates an
|
||||
executable that is a derivative of the Library (because it contains portions
|
||||
of the Library), rather than a "work that uses the library". The executable
|
||||
is therefore covered by this License. Section 6 states terms for distribution
|
||||
of such executables.
|
||||
|
||||
When a "work that uses the Library" uses material from a header file that
|
||||
is part of the Library, the object code for the work may be a derivative work
|
||||
of the Library even though the source code is not. Whether this is true is
|
||||
especially significant if the work can be linked without the Library, or if
|
||||
the work is itself a library. The threshold for this to be true is not precisely
|
||||
defined by law.
|
||||
|
||||
If such an object file uses only numerical parameters, data structure layouts
|
||||
and accessors, and small macros and small inline functions (ten lines or less
|
||||
in length), then the use of the object file is unrestricted, regardless of
|
||||
whether it is legally a derivative work. (Executables containing this object
|
||||
code plus portions of the Library will still fall under Section 6.)
|
||||
|
||||
Otherwise, if the work is a derivative of the Library, you may distribute
|
||||
the object code for the work under the terms of Section 6. Any executables
|
||||
containing that work also fall under Section 6, whether or not they are linked
|
||||
directly with the Library itself.
|
||||
|
||||
6. As an exception to the Sections above, you may also combine or link a "work
|
||||
that uses the Library" with the Library to produce a work containing portions
|
||||
of the Library, and distribute that work under terms of your choice, provided
|
||||
that the terms permit modification of the work for the customer's own use
|
||||
and reverse engineering for debugging such modifications.
|
||||
|
||||
You must give prominent notice with each copy of the work that the Library
|
||||
is used in it and that the Library and its use are covered by this License.
|
||||
You must supply a copy of this License. If the work during execution displays
|
||||
copyright notices, you must include the copyright notice for the Library among
|
||||
them, as well as a reference directing the user to the copy of this License.
|
||||
Also, you must do one of these things:
|
||||
|
||||
a) Accompany the work with the complete corresponding machine-readable source
|
||||
code for the Library including whatever changes were used in the work (which
|
||||
must be distributed under Sections 1 and 2 above); and, if the work is an
|
||||
executable linked with the Library, with the complete machine-readable "work
|
||||
that uses the Library", as object code and/or source code, so that the user
|
||||
can modify the Library and then relink to produce a modified executable containing
|
||||
the modified Library. (It is understood that the user who changes the contents
|
||||
of definitions files in the Library will not necessarily be able to recompile
|
||||
the application to use the modified definitions.)
|
||||
|
||||
b) Use a suitable shared library mechanism for linking with the Library. A
|
||||
suitable mechanism is one that (1) uses at run time a copy of the library
|
||||
already present on the user's computer system, rather than copying library
|
||||
functions into the executable, and (2) will operate properly with a modified
|
||||
version of the library, if the user installs one, as long as the modified
|
||||
version is interface-compatible with the version that the work was made with.
|
||||
|
||||
c) Accompany the work with a written offer, valid for at least three years,
|
||||
to give the same user the materials specified in Subsection 6a, above, for
|
||||
a charge no more than the cost of performing this distribution.
|
||||
|
||||
d) If distribution of the work is made by offering access to copy from a designated
|
||||
place, offer equivalent access to copy the above specified materials from
|
||||
the same place.
|
||||
|
||||
e) Verify that the user has already received a copy of these materials or
|
||||
that you have already sent this user a copy.
|
||||
|
||||
For an executable, the required form of the "work that uses the Library" must
|
||||
include any data and utility programs needed for reproducing the executable
|
||||
from it. However, as a special exception, the materials to be distributed
|
||||
need not include anything that is normally distributed (in either source or
|
||||
binary form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component itself
|
||||
accompanies the executable.
|
||||
|
||||
It may happen that this requirement contradicts the license restrictions of
|
||||
other proprietary libraries that do not normally accompany the operating system.
|
||||
Such a contradiction means you cannot use both them and the Library together
|
||||
in an executable that you distribute.
|
||||
|
||||
7. You may place library facilities that are a work based on the Library side-by-side
|
||||
in a single library together with other library facilities not covered by
|
||||
this License, and distribute such a combined library, provided that the separate
|
||||
distribution of the work based on the Library and of the other library facilities
|
||||
is otherwise permitted, and provided that you do these two things:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based on the
|
||||
Library, uncombined with any other library facilities. This must be distributed
|
||||
under the terms of the Sections above.
|
||||
|
||||
b) Give prominent notice with the combined library of the fact that part of
|
||||
it is a work based on the Library, and explaining where to find the accompanying
|
||||
uncombined form of the same work.
|
||||
|
||||
8. You may not copy, modify, sublicense, link with, or distribute the Library
|
||||
except as expressly provided under this License. Any attempt otherwise to
|
||||
copy, modify, sublicense, link with, or distribute the Library is void, and
|
||||
will automatically terminate your rights under this License. However, parties
|
||||
who have received copies, or rights, from you under this License will not
|
||||
have their licenses terminated so long as such parties remain in full compliance.
|
||||
|
||||
9. You are not required to accept this License, since you have not signed
|
||||
it. However, nothing else grants you permission to modify or distribute the
|
||||
Library or its derivative works. These actions are prohibited by law if you
|
||||
do not accept this License. Therefore, by modifying or distributing the Library
|
||||
(or any work based on the Library), you indicate your acceptance of this License
|
||||
to do so, and all its terms and conditions for copying, distributing or modifying
|
||||
the Library or works based on it.
|
||||
|
||||
10. Each time you redistribute the Library (or any work based on the Library),
|
||||
the recipient automatically receives a license from the original licensor
|
||||
to copy, distribute, link with or modify the Library subject to these terms
|
||||
and conditions. You may not impose any further restrictions on the recipients'
|
||||
exercise of the rights granted herein. You are not responsible for enforcing
|
||||
compliance by third parties with this License.
|
||||
|
||||
11. If, as a consequence of a court judgment or allegation of patent infringement
|
||||
or for any other reason (not limited to patent issues), conditions are imposed
|
||||
on you (whether by court order, agreement or otherwise) that contradict the
|
||||
conditions of this License, they do not excuse you from the conditions of
|
||||
this License. If you cannot distribute so as to satisfy simultaneously your
|
||||
obligations under this License and any other pertinent obligations, then as
|
||||
a consequence you may not distribute the Library at all. For example, if a
|
||||
patent license would not permit royalty-free redistribution of the Library
|
||||
by all those who receive copies directly or indirectly through you, then the
|
||||
only way you could satisfy both it and this License would be to refrain entirely
|
||||
from distribution of the Library.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any
|
||||
particular circumstance, the balance of the section is intended to apply,
|
||||
and the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any patents
|
||||
or other property right claims or to contest validity of any such claims;
|
||||
this section has the sole purpose of protecting the integrity of the free
|
||||
software distribution system which is implemented by public license practices.
|
||||
Many people have made generous contributions to the wide range of software
|
||||
distributed through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing to
|
||||
distribute software through any other system and a licensee cannot impose
|
||||
that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to be a
|
||||
consequence of the rest of this License.
|
||||
|
||||
12. If the distribution and/or use of the Library is restricted in certain
|
||||
countries either by patents or by copyrighted interfaces, the original copyright
|
||||
holder who places the Library under this License may add an explicit geographical
|
||||
distribution limitation excluding those countries, so that distribution is
|
||||
permitted only in or among countries not thus excluded. In such case, this
|
||||
License incorporates the limitation as if written in the body of this License.
|
||||
|
||||
13. The Free Software Foundation may publish revised and/or new versions of
|
||||
the Lesser General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to address
|
||||
new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Library specifies
|
||||
a version number of this License which applies to it and "any later version",
|
||||
you have the option of following the terms and conditions either of that version
|
||||
or of any later version published by the Free Software Foundation. If the
|
||||
Library does not specify a license version number, you may choose any version
|
||||
ever published by the Free Software Foundation.
|
||||
|
||||
14. If you wish to incorporate parts of the Library into other free programs
|
||||
whose distribution conditions are incompatible with these, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free Software
|
||||
Foundation, write to the Free Software Foundation; we sometimes make exceptions
|
||||
for this. Our decision will be guided by the two goals of preserving the free
|
||||
status of all derivatives of our free software and of promoting the sharing
|
||||
and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
|
||||
THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
|
||||
STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY
|
||||
"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
|
||||
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE
|
||||
OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE
|
||||
THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE
|
||||
OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA
|
||||
OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES
|
||||
OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH
|
||||
HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Libraries
|
||||
|
||||
If you develop a new library, and you want it to be of the greatest possible
|
||||
use to the public, we recommend making it free software that everyone can
|
||||
redistribute and change. You can do so by permitting redistribution under
|
||||
these terms (or, alternatively, under the terms of the ordinary General Public
|
||||
License).
|
||||
|
||||
To apply these terms, attach the following notices to the library. It is safest
|
||||
to attach them to the start of each source file to most effectively convey
|
||||
the exclusion of warranty; and each file should have at least the "copyright"
|
||||
line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the library's name and an idea of what it does.>
|
||||
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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; if not, write to the Free Software Foundation, Inc., 51
|
||||
Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your school,
|
||||
if any, to sign a "copyright disclaimer" for the library, if necessary. Here
|
||||
is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in
|
||||
|
||||
the library `Frob' (a library for tweaking knobs) written
|
||||
|
||||
by James Random Hacker.
|
||||
|
||||
< signature of Ty Coon > , 1 April 1990
|
||||
|
||||
Ty Coon, President of Vice
|
||||
|
||||
That's all there is to it!
|
||||
@@ -0,0 +1,468 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
|
||||
Version 2.1, February 1999
|
||||
|
||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this license
|
||||
document, but changing it is not allowed.
|
||||
|
||||
[This is the first released version of the Lesser GPL. It also counts as the
|
||||
successor of the GNU Library Public License, version 2, hence the version
|
||||
number 2.1.]
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your freedom to share
|
||||
and change it. By contrast, the GNU General Public Licenses are intended to
|
||||
guarantee your freedom to share and change free software--to make sure the
|
||||
software is free for all its users.
|
||||
|
||||
This license, the Lesser General Public License, applies to some specially
|
||||
designated software packages--typically libraries--of the Free Software Foundation
|
||||
and other authors who decide to use it. You can use it too, but we suggest
|
||||
you first think carefully about whether this license or the ordinary General
|
||||
Public License is the better strategy to use in any particular case, based
|
||||
on the explanations below.
|
||||
|
||||
When we speak of free software, we are referring to freedom of use, not price.
|
||||
Our General Public Licenses are designed to make sure that you have the freedom
|
||||
to distribute copies of free software (and charge for this service if you
|
||||
wish); that you receive source code or can get it if you want it; that you
|
||||
can change the software and use pieces of it in new free programs; and that
|
||||
you are informed that you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid distributors
|
||||
to deny you these rights or to ask you to surrender these rights. These restrictions
|
||||
translate to certain responsibilities for you if you distribute copies of
|
||||
the library or if you modify it.
|
||||
|
||||
For example, if you distribute copies of the library, whether gratis or for
|
||||
a fee, you must give the recipients all the rights that we gave you. You must
|
||||
make sure that they, too, receive or can get the source code. If you link
|
||||
other code with the library, you must provide complete object files to the
|
||||
recipients, so that they can relink them with the library after making changes
|
||||
to the library and recompiling it. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
We protect your rights with a two-step method: (1) we copyright the library,
|
||||
and (2) we offer you this license, which gives you legal permission to copy,
|
||||
distribute and/or modify the library.
|
||||
|
||||
To protect each distributor, we want to make it very clear that there is no
|
||||
warranty for the free library. Also, if the library is modified by someone
|
||||
else and passed on, the recipients should know that what they have is not
|
||||
the original version, so that the original author's reputation will not be
|
||||
affected by problems that might be introduced by others.
|
||||
|
||||
Finally, software patents pose a constant threat to the existence of any free
|
||||
program. We wish to make sure that a company cannot effectively restrict the
|
||||
users of a free program by obtaining a restrictive license from a patent holder.
|
||||
Therefore, we insist that any patent license obtained for a version of the
|
||||
library must be consistent with the full freedom of use specified in this
|
||||
license.
|
||||
|
||||
Most GNU software, including some libraries, is covered by the ordinary GNU
|
||||
General Public License. This license, the GNU Lesser General Public License,
|
||||
applies to certain designated libraries, and is quite different from the ordinary
|
||||
General Public License. We use this license for certain libraries in order
|
||||
to permit linking those libraries into non-free programs.
|
||||
|
||||
When a program is linked with a library, whether statically or using a shared
|
||||
library, the combination of the two is legally speaking a combined work, a
|
||||
derivative of the original library. The ordinary General Public License therefore
|
||||
permits such linking only if the entire combination fits its criteria of freedom.
|
||||
The Lesser General Public License permits more lax criteria for linking other
|
||||
code with the library.
|
||||
|
||||
We call this license the "Lesser" General Public License because it does Less
|
||||
to protect the user's freedom than the ordinary General Public License. It
|
||||
also provides other free software developers Less of an advantage over competing
|
||||
non-free programs. These disadvantages are the reason we use the ordinary
|
||||
General Public License for many libraries. However, the Lesser license provides
|
||||
advantages in certain special circumstances.
|
||||
|
||||
For example, on rare occasions, there may be a special need to encourage the
|
||||
widest possible use of a certain library, so that it becomes a de-facto standard.
|
||||
To achieve this, non-free programs must be allowed to use the library. A more
|
||||
frequent case is that a free library does the same job as widely used non-free
|
||||
libraries. In this case, there is little to gain by limiting the free library
|
||||
to free software only, so we use the Lesser General Public License.
|
||||
|
||||
In other cases, permission to use a particular library in non-free programs
|
||||
enables a greater number of people to use a large body of free software. For
|
||||
example, permission to use the GNU C Library in non-free programs enables
|
||||
many more people to use the whole GNU operating system, as well as its variant,
|
||||
the GNU/Linux operating system.
|
||||
|
||||
Although the Lesser General Public License is Less protective of the users'
|
||||
freedom, it does ensure that the user of a program that is linked with the
|
||||
Library has the freedom and the wherewithal to run that program using a modified
|
||||
version of the Library.
|
||||
|
||||
The precise terms and conditions for copying, distribution and modification
|
||||
follow. Pay close attention to the difference between a "work based on the
|
||||
library" and a "work that uses the library". The former contains code derived
|
||||
from the library, whereas the latter must be combined with the library in
|
||||
order to run.
|
||||
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License Agreement applies to any software library or other program
|
||||
which contains a notice placed by the copyright holder or other authorized
|
||||
party saying it may be distributed under the terms of this Lesser General
|
||||
Public License (also called "this License"). Each licensee is addressed as
|
||||
"you".
|
||||
|
||||
A "library" means a collection of software functions and/or data prepared
|
||||
so as to be conveniently linked with application programs (which use some
|
||||
of those functions and data) to form executables.
|
||||
|
||||
The "Library", below, refers to any such software library or work which has
|
||||
been distributed under these terms. A "work based on the Library" means either
|
||||
the Library or any derivative work under copyright law: that is to say, a
|
||||
work containing the Library or a portion of it, either verbatim or with modifications
|
||||
and/or translated straightforwardly into another language. (Hereinafter, translation
|
||||
is included without limitation in the term "modification".)
|
||||
|
||||
"Source code" for a work means the preferred form of the work for making modifications
|
||||
to it. For a library, complete source code means all the source code for all
|
||||
modules it contains, plus any associated interface definition files, plus
|
||||
the scripts used to control compilation and installation of the library.
|
||||
|
||||
Activities other than copying, distribution and modification are not covered
|
||||
by this License; they are outside its scope. The act of running a program
|
||||
using the Library is not restricted, and output from such a program is covered
|
||||
only if its contents constitute a work based on the Library (independent of
|
||||
the use of the Library in a tool for writing it). Whether that is true depends
|
||||
on what the Library does and what the program that uses the Library does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Library's complete source
|
||||
code as you receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice and disclaimer
|
||||
of warranty; keep intact all the notices that refer to this License and to
|
||||
the absence of any warranty; and distribute a copy of this License along with
|
||||
the Library.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and you
|
||||
may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Library or any portion of it,
|
||||
thus forming a work based on the Library, and copy and distribute such modifications
|
||||
or work under the terms of Section 1 above, provided that you also meet all
|
||||
of these conditions:
|
||||
|
||||
a) The modified work must itself be a software library.
|
||||
|
||||
b) You must cause the files modified to carry prominent notices stating that
|
||||
you changed the files and the date of any change.
|
||||
|
||||
c) You must cause the whole of the work to be licensed at no charge to all
|
||||
third parties under the terms of this License.
|
||||
|
||||
d) If a facility in the modified Library refers to a function or a table of
|
||||
data to be supplied by an application program that uses the facility, other
|
||||
than as an argument passed when the facility is invoked, then you must make
|
||||
a good faith effort to ensure that, in the event an application does not supply
|
||||
such function or table, the facility still operates, and performs whatever
|
||||
part of its purpose remains meaningful.
|
||||
|
||||
(For example, a function in a library to compute square roots has a purpose
|
||||
that is entirely well-defined independent of the application. Therefore, Subsection
|
||||
2d requires that any application-supplied function or table used by this function
|
||||
must be optional: if the application does not supply it, the square root function
|
||||
must still compute square roots.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If identifiable
|
||||
sections of that work are not derived from the Library, and can be reasonably
|
||||
considered independent and separate works in themselves, then this License,
|
||||
and its terms, do not apply to those sections when you distribute them as
|
||||
separate works. But when you distribute the same sections as part of a whole
|
||||
which is a work based on the Library, the distribution of the whole must be
|
||||
on the terms of this License, whose permissions for other licensees extend
|
||||
to the entire whole, and thus to each and every part regardless of who wrote
|
||||
it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest your
|
||||
rights to work written entirely by you; rather, the intent is to exercise
|
||||
the right to control the distribution of derivative or collective works based
|
||||
on the Library.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Library with
|
||||
the Library (or with a work based on the Library) on a volume of a storage
|
||||
or distribution medium does not bring the other work under the scope of this
|
||||
License.
|
||||
|
||||
3. You may opt to apply the terms of the ordinary GNU General Public License
|
||||
instead of this License to a given copy of the Library. To do this, you must
|
||||
alter all the notices that refer to this License, so that they refer to the
|
||||
ordinary GNU General Public License, version 2, instead of to this License.
|
||||
(If a newer version than version 2 of the ordinary GNU General Public License
|
||||
has appeared, then you can specify that version instead if you wish.) Do not
|
||||
make any other change in these notices.
|
||||
|
||||
Once this change is made in a given copy, it is irreversible for that copy,
|
||||
so the ordinary GNU General Public License applies to all subsequent copies
|
||||
and derivative works made from that copy.
|
||||
|
||||
This option is useful when you wish to copy part of the code of the Library
|
||||
into a program that is not a library.
|
||||
|
||||
4. You may copy and distribute the Library (or a portion or derivative of
|
||||
it, under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you accompany it with the complete corresponding
|
||||
machine-readable source code, which must be distributed under the terms of
|
||||
Sections 1 and 2 above on a medium customarily used for software interchange.
|
||||
|
||||
If distribution of object code is made by offering access to copy from a designated
|
||||
place, then offering equivalent access to copy the source code from the same
|
||||
place satisfies the requirement to distribute the source code, even though
|
||||
third parties are not compelled to copy the source along with the object code.
|
||||
|
||||
5. A program that contains no derivative of any portion of the Library, but
|
||||
is designed to work with the Library by being compiled or linked with it,
|
||||
is called a "work that uses the Library". Such a work, in isolation, is not
|
||||
a derivative work of the Library, and therefore falls outside the scope of
|
||||
this License.
|
||||
|
||||
However, linking a "work that uses the Library" with the Library creates an
|
||||
executable that is a derivative of the Library (because it contains portions
|
||||
of the Library), rather than a "work that uses the library". The executable
|
||||
is therefore covered by this License. Section 6 states terms for distribution
|
||||
of such executables.
|
||||
|
||||
When a "work that uses the Library" uses material from a header file that
|
||||
is part of the Library, the object code for the work may be a derivative work
|
||||
of the Library even though the source code is not. Whether this is true is
|
||||
especially significant if the work can be linked without the Library, or if
|
||||
the work is itself a library. The threshold for this to be true is not precisely
|
||||
defined by law.
|
||||
|
||||
If such an object file uses only numerical parameters, data structure layouts
|
||||
and accessors, and small macros and small inline functions (ten lines or less
|
||||
in length), then the use of the object file is unrestricted, regardless of
|
||||
whether it is legally a derivative work. (Executables containing this object
|
||||
code plus portions of the Library will still fall under Section 6.)
|
||||
|
||||
Otherwise, if the work is a derivative of the Library, you may distribute
|
||||
the object code for the work under the terms of Section 6. Any executables
|
||||
containing that work also fall under Section 6, whether or not they are linked
|
||||
directly with the Library itself.
|
||||
|
||||
6. As an exception to the Sections above, you may also combine or link a "work
|
||||
that uses the Library" with the Library to produce a work containing portions
|
||||
of the Library, and distribute that work under terms of your choice, provided
|
||||
that the terms permit modification of the work for the customer's own use
|
||||
and reverse engineering for debugging such modifications.
|
||||
|
||||
You must give prominent notice with each copy of the work that the Library
|
||||
is used in it and that the Library and its use are covered by this License.
|
||||
You must supply a copy of this License. If the work during execution displays
|
||||
copyright notices, you must include the copyright notice for the Library among
|
||||
them, as well as a reference directing the user to the copy of this License.
|
||||
Also, you must do one of these things:
|
||||
|
||||
a) Accompany the work with the complete corresponding machine-readable source
|
||||
code for the Library including whatever changes were used in the work (which
|
||||
must be distributed under Sections 1 and 2 above); and, if the work is an
|
||||
executable linked with the Library, with the complete machine-readable "work
|
||||
that uses the Library", as object code and/or source code, so that the user
|
||||
can modify the Library and then relink to produce a modified executable containing
|
||||
the modified Library. (It is understood that the user who changes the contents
|
||||
of definitions files in the Library will not necessarily be able to recompile
|
||||
the application to use the modified definitions.)
|
||||
|
||||
b) Use a suitable shared library mechanism for linking with the Library. A
|
||||
suitable mechanism is one that (1) uses at run time a copy of the library
|
||||
already present on the user's computer system, rather than copying library
|
||||
functions into the executable, and (2) will operate properly with a modified
|
||||
version of the library, if the user installs one, as long as the modified
|
||||
version is interface-compatible with the version that the work was made with.
|
||||
|
||||
c) Accompany the work with a written offer, valid for at least three years,
|
||||
to give the same user the materials specified in Subsection 6a, above, for
|
||||
a charge no more than the cost of performing this distribution.
|
||||
|
||||
d) If distribution of the work is made by offering access to copy from a designated
|
||||
place, offer equivalent access to copy the above specified materials from
|
||||
the same place.
|
||||
|
||||
e) Verify that the user has already received a copy of these materials or
|
||||
that you have already sent this user a copy.
|
||||
|
||||
For an executable, the required form of the "work that uses the Library" must
|
||||
include any data and utility programs needed for reproducing the executable
|
||||
from it. However, as a special exception, the materials to be distributed
|
||||
need not include anything that is normally distributed (in either source or
|
||||
binary form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component itself
|
||||
accompanies the executable.
|
||||
|
||||
It may happen that this requirement contradicts the license restrictions of
|
||||
other proprietary libraries that do not normally accompany the operating system.
|
||||
Such a contradiction means you cannot use both them and the Library together
|
||||
in an executable that you distribute.
|
||||
|
||||
7. You may place library facilities that are a work based on the Library side-by-side
|
||||
in a single library together with other library facilities not covered by
|
||||
this License, and distribute such a combined library, provided that the separate
|
||||
distribution of the work based on the Library and of the other library facilities
|
||||
is otherwise permitted, and provided that you do these two things:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based on the
|
||||
Library, uncombined with any other library facilities. This must be distributed
|
||||
under the terms of the Sections above.
|
||||
|
||||
b) Give prominent notice with the combined library of the fact that part of
|
||||
it is a work based on the Library, and explaining where to find the accompanying
|
||||
uncombined form of the same work.
|
||||
|
||||
8. You may not copy, modify, sublicense, link with, or distribute the Library
|
||||
except as expressly provided under this License. Any attempt otherwise to
|
||||
copy, modify, sublicense, link with, or distribute the Library is void, and
|
||||
will automatically terminate your rights under this License. However, parties
|
||||
who have received copies, or rights, from you under this License will not
|
||||
have their licenses terminated so long as such parties remain in full compliance.
|
||||
|
||||
9. You are not required to accept this License, since you have not signed
|
||||
it. However, nothing else grants you permission to modify or distribute the
|
||||
Library or its derivative works. These actions are prohibited by law if you
|
||||
do not accept this License. Therefore, by modifying or distributing the Library
|
||||
(or any work based on the Library), you indicate your acceptance of this License
|
||||
to do so, and all its terms and conditions for copying, distributing or modifying
|
||||
the Library or works based on it.
|
||||
|
||||
10. Each time you redistribute the Library (or any work based on the Library),
|
||||
the recipient automatically receives a license from the original licensor
|
||||
to copy, distribute, link with or modify the Library subject to these terms
|
||||
and conditions. You may not impose any further restrictions on the recipients'
|
||||
exercise of the rights granted herein. You are not responsible for enforcing
|
||||
compliance by third parties with this License.
|
||||
|
||||
11. If, as a consequence of a court judgment or allegation of patent infringement
|
||||
or for any other reason (not limited to patent issues), conditions are imposed
|
||||
on you (whether by court order, agreement or otherwise) that contradict the
|
||||
conditions of this License, they do not excuse you from the conditions of
|
||||
this License. If you cannot distribute so as to satisfy simultaneously your
|
||||
obligations under this License and any other pertinent obligations, then as
|
||||
a consequence you may not distribute the Library at all. For example, if a
|
||||
patent license would not permit royalty-free redistribution of the Library
|
||||
by all those who receive copies directly or indirectly through you, then the
|
||||
only way you could satisfy both it and this License would be to refrain entirely
|
||||
from distribution of the Library.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any
|
||||
particular circumstance, the balance of the section is intended to apply,
|
||||
and the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any patents
|
||||
or other property right claims or to contest validity of any such claims;
|
||||
this section has the sole purpose of protecting the integrity of the free
|
||||
software distribution system which is implemented by public license practices.
|
||||
Many people have made generous contributions to the wide range of software
|
||||
distributed through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing to
|
||||
distribute software through any other system and a licensee cannot impose
|
||||
that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to be a
|
||||
consequence of the rest of this License.
|
||||
|
||||
12. If the distribution and/or use of the Library is restricted in certain
|
||||
countries either by patents or by copyrighted interfaces, the original copyright
|
||||
holder who places the Library under this License may add an explicit geographical
|
||||
distribution limitation excluding those countries, so that distribution is
|
||||
permitted only in or among countries not thus excluded. In such case, this
|
||||
License incorporates the limitation as if written in the body of this License.
|
||||
|
||||
13. The Free Software Foundation may publish revised and/or new versions of
|
||||
the Lesser General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to address
|
||||
new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Library specifies
|
||||
a version number of this License which applies to it and "any later version",
|
||||
you have the option of following the terms and conditions either of that version
|
||||
or of any later version published by the Free Software Foundation. If the
|
||||
Library does not specify a license version number, you may choose any version
|
||||
ever published by the Free Software Foundation.
|
||||
|
||||
14. If you wish to incorporate parts of the Library into other free programs
|
||||
whose distribution conditions are incompatible with these, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free Software
|
||||
Foundation, write to the Free Software Foundation; we sometimes make exceptions
|
||||
for this. Our decision will be guided by the two goals of preserving the free
|
||||
status of all derivatives of our free software and of promoting the sharing
|
||||
and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
|
||||
THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
|
||||
STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY
|
||||
"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
|
||||
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE
|
||||
OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE
|
||||
THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE
|
||||
OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA
|
||||
OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES
|
||||
OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH
|
||||
HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Libraries
|
||||
|
||||
If you develop a new library, and you want it to be of the greatest possible
|
||||
use to the public, we recommend making it free software that everyone can
|
||||
redistribute and change. You can do so by permitting redistribution under
|
||||
these terms (or, alternatively, under the terms of the ordinary General Public
|
||||
License).
|
||||
|
||||
To apply these terms, attach the following notices to the library. It is safest
|
||||
to attach them to the start of each source file to most effectively convey
|
||||
the exclusion of warranty; and each file should have at least the "copyright"
|
||||
line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the library's name and an idea of what it does.>
|
||||
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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; if not, write to the Free Software Foundation, Inc., 51
|
||||
Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your school,
|
||||
if any, to sign a "copyright disclaimer" for the library, if necessary. Here
|
||||
is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in
|
||||
|
||||
the library `Frob' (a library for tweaking knobs) written
|
||||
|
||||
by James Random Hacker.
|
||||
|
||||
< signature of Ty Coon > , 1 April 1990
|
||||
|
||||
Ty Coon, President of Vice
|
||||
|
||||
That's all there is to it!
|
||||
@@ -0,0 +1,15 @@
|
||||
# Notice
|
||||
|
||||
`src/` is a modified copy of the `classicui` addon of
|
||||
[Fcitx5](https://github.com/fcitx/fcitx5) 5.1.23
|
||||
(`src/ui/classic`), copyright the Fcitx5 authors including CSSlayer, licensed
|
||||
under LGPL-2.1-or-later. Files keep their original SPDX headers. The changes are
|
||||
the renamed addon, the Material 3 painting, the animations and the candidate
|
||||
action menu.
|
||||
|
||||
The themes in `themes/` are derived from the layout values of a local theme and
|
||||
use the same license.
|
||||
|
||||
`kwin/install-fadingpopups-override.sh` patches a copy of KWin's *Fading Popups*
|
||||
effect (GPL-2.0-or-later, KDE) on the user's machine. No KWin code is included
|
||||
in this repository.
|
||||
@@ -0,0 +1,98 @@
|
||||
# fcitx5-m3ui
|
||||
|
||||
A Material 3 styled candidate window for [Fcitx5](https://github.com/fcitx/fcitx5),
|
||||
built as a user interface addon (`m3ui`) that replaces `classicui`.
|
||||
|
||||
[简体中文](README.zh-CN.md)
|
||||
|
||||
## Features
|
||||
|
||||
- Rounded Material 3 panel with light and dark themes that follow the system.
|
||||
- Spring based animations: the panel slides in with a small bounce and fades in,
|
||||
the highlight pill glides between candidates. Stiffness and damping are
|
||||
configurable.
|
||||
- Right click a candidate to open an action menu. It shows whatever actions the
|
||||
input method offers, for Pinyin that is *Forget candidate*, *Pin to top as
|
||||
custom phrase* and *Delete from custom phrase*.
|
||||
- Works on Wayland and X11 (Xwayland) input contexts, positioning and pointer
|
||||
handling are inherited from `classicui`.
|
||||
- Optional KWin helper that shortens KWin's own fade out for the candidate
|
||||
window only.
|
||||
|
||||
## Status
|
||||
|
||||
Personal project, only tried on Fedora 44, KDE Plasma 6.7 on Wayland, NVIDIA
|
||||
GPU, Fcitx5 5.1.23 with the Pinyin engine. The build scripts have not been run
|
||||
on any other setup.
|
||||
|
||||
The addon is a fork of `classicui` and is built inside the Fcitx5 source tree,
|
||||
because Fcitx5's Wayland protocol wrappers are not installed by `fcitx5-devel`.
|
||||
The Fcitx5 source version used for the build must match the installed Fcitx5.
|
||||
|
||||
There is no fade out animation of its own: on Wayland the window is destroyed as
|
||||
soon as the panel has nothing to show.
|
||||
|
||||
## Build
|
||||
|
||||
Needs the Fcitx5 development files and the dependencies of `classicui`
|
||||
(cairo, pango, librsvg, gdk-pixbuf, wayland-protocols, xcb libraries). On Fedora:
|
||||
|
||||
```
|
||||
sudo dnf install fcitx5-devel extra-cmake-modules cairo-devel pango-devel \
|
||||
gdk-pixbuf2-devel librsvg2-devel wayland-protocols-devel \
|
||||
plasma-wayland-protocols-devel systemd-devel libuuid-devel fmt-devel \
|
||||
expat-devel iso-codes-devel json-devel xkeyboard-config-devel \
|
||||
xcb-util-devel xcb-util-keysyms-devel xcb-util-wm-devel \
|
||||
xcb-util-image-devel xcb-imdkit-devel libxkbfile-devel libxkbcommon-x11-devel
|
||||
```
|
||||
|
||||
then
|
||||
|
||||
```
|
||||
scripts/build.sh # downloads Fcitx5 + yoga, overlays src/, builds m3ui
|
||||
scripts/install.sh # installs into ~/.local, nothing outside $HOME
|
||||
```
|
||||
|
||||
`build.sh` picks the Fcitx5 version from `pkg-config`, override it with
|
||||
`FCITX_VERSION`.
|
||||
|
||||
## Use
|
||||
|
||||
```
|
||||
FCITX_ADDON_DIRS=$HOME/.local/m3ui/lib64/fcitx5:/usr/lib64/fcitx5 fcitx5 -r -u m3ui
|
||||
```
|
||||
|
||||
`FCITX_ADDON_DIRS` is how Fcitx5 finds addons outside the system directory, it
|
||||
has to be set in the environment that starts Fcitx5 if you want this on every
|
||||
login. `fcitx5 -r` brings the stock interface back.
|
||||
|
||||
Settings live in `~/.config/fcitx5/conf/m3ui.conf`, see
|
||||
[config/m3ui.conf.example](config/m3ui.conf.example).
|
||||
|
||||
| Key | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `Animations` | True | Master switch for animations |
|
||||
| `Bounce` | True | Off makes movement critically damped (no overshoot) |
|
||||
| `SpatialStiffness` | 2000 | Spring stiffness for movement, higher is faster |
|
||||
| `SpatialDamping` | 62 | Damping ratio in percent, lower bounces more |
|
||||
| `EffectsStiffness` | 3800 | Spring stiffness for fading |
|
||||
| `SlideDistance` | 22 | Slide in distance in px |
|
||||
| `SlideDirection` | 0 | 0 from left, 1 from right, 2 from below |
|
||||
| `Theme` / `DarkTheme` | `m3ui-light` / `m3ui-dark` | Themes in `themes/` |
|
||||
|
||||
Themes are classicui themes with extra keys in `[InputPanel]`: `SurfaceColor`,
|
||||
`OutlineColor`, `HighlightFillColor`, `PanelRadius`, `ItemRadius`. A transparent
|
||||
`SurfaceColor` falls back to the classic image based drawing, so classic themes
|
||||
still work (without animation).
|
||||
|
||||
## KWin fade out (optional, KDE Plasma)
|
||||
|
||||
KWin's *Fading Popups* effect fades popups out over 600 ms and the candidate
|
||||
window counts as a popup. `kwin/install-fadingpopups-override.sh` copies the
|
||||
effect from your system into `~/.local/share/kwin-wayland/effects/` and patches
|
||||
the copy so only the window class `fcitx fcitx` is affected. The script fails
|
||||
if the upstream effect source no longer matches what it patches.
|
||||
|
||||
## License
|
||||
|
||||
LGPL-2.1-or-later, see [NOTICE.md](NOTICE.md).
|
||||
@@ -0,0 +1,41 @@
|
||||
# fcitx5-m3ui
|
||||
|
||||
给 [Fcitx5](https://github.com/fcitx/fcitx5) 用的 Material 3 风格候选框,
|
||||
以界面插件(`m3ui`)的形式替换 `classicui`。
|
||||
|
||||
[English](README.md)
|
||||
|
||||
## 特性
|
||||
|
||||
- Material 3 圆角面板,深色/浅色主题跟随系统。
|
||||
- 弹簧动画:候选框带轻微回弹滑入并淡入,高亮块在候选词之间滑动,刚度和阻尼可调。
|
||||
- 在候选词上右键打开动作菜单,内容取决于输入法提供的动作,拼音有
|
||||
"忘记候选词 / 置顶为自定义短语 / 从自定义短语中删除"。
|
||||
- Wayland 和 X11(Xwayland)输入上下文都支持,定位和鼠标处理沿用 `classicui`。
|
||||
- 可选的 KWin 辅助脚本:只缩短候选框自己的 KWin 淡出时间。
|
||||
|
||||
## 状态
|
||||
|
||||
个人项目,只在 Fedora 44、KDE Plasma 6.7(Wayland)、NVIDIA 显卡、
|
||||
Fcitx5 5.1.23、拼音引擎上用过,构建脚本没有在其他环境跑过。
|
||||
|
||||
本插件是 `classicui` 的分支,需要在 Fcitx5 源码树内构建(Fcitx5 的 Wayland 协议封装
|
||||
不包含在 `fcitx5-devel` 里),构建用的源码版本必须和已安装的 Fcitx5 一致。
|
||||
|
||||
没有自己的淡出动画:Wayland 下面板无内容时窗口会被立刻销毁。
|
||||
|
||||
## 构建与使用
|
||||
|
||||
依赖安装命令、`scripts/build.sh`、`scripts/install.sh` 以及启动方式见
|
||||
[English README](README.md)。启动命令:
|
||||
|
||||
```
|
||||
FCITX_ADDON_DIRS=$HOME/.local/m3ui/lib64/fcitx5:/usr/lib64/fcitx5 fcitx5 -r -u m3ui
|
||||
```
|
||||
|
||||
想回到原版界面运行 `fcitx5 -r`。配置在 `~/.config/fcitx5/conf/m3ui.conf`,
|
||||
示例见 [config/m3ui.conf.example](config/m3ui.conf.example)。
|
||||
|
||||
## 许可证
|
||||
|
||||
LGPL-2.1-or-later,见 [NOTICE.md](NOTICE.md)。
|
||||
@@ -0,0 +1,19 @@
|
||||
Vertical Candidate List=False
|
||||
WheelForPaging=True
|
||||
Font="Noto Sans CJK SC 11"
|
||||
MenuFont="Noto Sans CJK SC 10"
|
||||
TrayFont="Noto Sans CJK SC 10"
|
||||
Theme=m3ui-light
|
||||
DarkTheme=m3ui-dark
|
||||
UseDarkTheme=True
|
||||
UseAccentColor=False
|
||||
PerScreenDPI=True
|
||||
ForceWaylandDPI=0
|
||||
EnableFractionalScale=True
|
||||
Animations=True
|
||||
Bounce=True
|
||||
SpatialStiffness=2000
|
||||
SpatialDamping=62
|
||||
EffectsStiffness=3800
|
||||
SlideDistance=22
|
||||
SlideDirection=0
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/bin/bash
|
||||
# Optional. KWin's "Fading Popups" effect fades popup windows in over 150 ms
|
||||
# and out over 600 ms, and the Fcitx5 candidate window counts as a popup. This
|
||||
# script copies the effect from the system into your home directory and
|
||||
# patches the copy so that only the Fcitx5 window ("fcitx fcitx") skips the
|
||||
# fade in and uses a short fade out. All other popups are untouched.
|
||||
#
|
||||
# The effect is KWin's code (GPL-2.0-or-later), so it is generated from the
|
||||
# copy installed on your system instead of being shipped in this repository.
|
||||
set -euo pipefail
|
||||
|
||||
fade_out_ms=${FADE_OUT_MS:-90}
|
||||
src=
|
||||
for dir in /usr/share/kwin-wayland/effects/fadingpopups /usr/share/kwin/effects/fadingpopups; do
|
||||
[ -d "$dir" ] && src=$dir && break
|
||||
done
|
||||
[ -n "$src" ] || { echo "KWin fadingpopups effect not found" >&2; exit 1; }
|
||||
|
||||
dst=${XDG_DATA_HOME:-$HOME/.local/share}/kwin-wayland/effects/fadingpopups
|
||||
rm -rf "$dst"
|
||||
mkdir -p "$(dirname "$dst")"
|
||||
cp -r "$src" "$dst"
|
||||
|
||||
python3 - "$dst/contents/code/main.js" "$fade_out_ms" <<'PY'
|
||||
import sys
|
||||
path, fade_out = sys.argv[1], sys.argv[2]
|
||||
s = open(path).read()
|
||||
|
||||
def rep(old, new):
|
||||
global s
|
||||
if old not in s:
|
||||
sys.exit("effect source changed upstream, cannot patch: " + old[:50])
|
||||
s = s.replace(old, new, 1)
|
||||
|
||||
rep("var fadingPopupsEffect = {", f'''// Added by m3ui: the Fcitx5 candidate window animates itself.
|
||||
var inputMethodWindowClass = "fcitx fcitx";
|
||||
var inputMethodFadeOutMs = {fade_out};
|
||||
|
||||
function isInputMethodWindow(window) {{
|
||||
return window.windowClass == inputMethodWindowClass;
|
||||
}}
|
||||
|
||||
var fadingPopupsEffect = {{''')
|
||||
rep(''' if (!isPopupWindow(window)) {
|
||||
return;
|
||||
}
|
||||
if (!window.visible) {
|
||||
return;
|
||||
}''', ''' if (!isPopupWindow(window) || isInputMethodWindow(window)) {
|
||||
return;
|
||||
}
|
||||
if (!window.visible) {
|
||||
return;
|
||||
}''')
|
||||
rep(" duration: fadingPopupsEffect.fadeOutDuration,",
|
||||
''' duration: isInputMethodWindow(window)
|
||||
? animationTime(inputMethodFadeOutMs)
|
||||
: fadingPopupsEffect.fadeOutDuration,''')
|
||||
open(path, "w").write(s)
|
||||
PY
|
||||
|
||||
if command -v gdbus >/dev/null; then
|
||||
gdbus call --session --dest org.kde.KWin --object-path /Effects \
|
||||
--method org.kde.kwin.Effects.unloadEffect fadingpopups >/dev/null || true
|
||||
gdbus call --session --dest org.kde.KWin --object-path /Effects \
|
||||
--method org.kde.kwin.Effects.loadEffect fadingpopups >/dev/null || true
|
||||
fi
|
||||
echo "Installed to $dst"
|
||||
echo "To undo: rm -rf '$dst' and reload the effect."
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
# Builds libm3ui.so against an upstream Fcitx5 source tree of the same version
|
||||
# as the Fcitx5 installed on this system. The addon needs Fcitx5's internal
|
||||
# Wayland protocol wrappers, which are not part of fcitx5-devel, so it is built
|
||||
# inside the upstream tree.
|
||||
set -euo pipefail
|
||||
|
||||
root=$(cd "$(dirname "$0")/.." && pwd)
|
||||
work=${WORK_DIR:-$root/build}
|
||||
version=${FCITX_VERSION:-$(pkg-config --modversion Fcitx5Core)}
|
||||
# Commit of the yoga submodule pinned by Fcitx5 (third_party/yoga).
|
||||
yoga_commit=${YOGA_COMMIT:-042f5013152eb81c1552dec945b88f7b95ca350f}
|
||||
|
||||
srcdir=$work/fcitx5-$version
|
||||
mkdir -p "$work"
|
||||
|
||||
if [ ! -d "$srcdir" ]; then
|
||||
echo ">> fetching Fcitx5 $version"
|
||||
curl -fsSL "https://github.com/fcitx/fcitx5/archive/refs/tags/$version.tar.gz" |
|
||||
tar -xz -C "$work"
|
||||
fi
|
||||
if [ ! -f "$srcdir/third_party/yoga/yoga/CMakeLists.txt" ]; then
|
||||
echo ">> fetching yoga $yoga_commit"
|
||||
rm -rf "$srcdir/third_party/yoga"
|
||||
mkdir -p "$srcdir/third_party/yoga"
|
||||
curl -fsSL "https://github.com/facebook/yoga/archive/$yoga_commit.tar.gz" |
|
||||
tar -xz -C "$srcdir/third_party/yoga" --strip-components=1
|
||||
fi
|
||||
|
||||
echo ">> overlaying m3ui"
|
||||
rm -rf "$srcdir/src/ui/m3ui"
|
||||
cp -r "$root/src" "$srcdir/src/ui/m3ui"
|
||||
if ! grep -q 'add_subdirectory(m3ui)' "$srcdir/src/ui/CMakeLists.txt"; then
|
||||
sed -i 's/^add_subdirectory(classic)$/add_subdirectory(classic)\nadd_subdirectory(m3ui)/' \
|
||||
"$srcdir/src/ui/CMakeLists.txt"
|
||||
fi
|
||||
|
||||
echo ">> configuring"
|
||||
cmake -S "$srcdir" -B "$work/cmake" -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX="${PREFIX:-$HOME/.local/m3ui}" \
|
||||
-DENABLE_X11=On -DENABLE_WAYLAND=On \
|
||||
-DENABLE_TEST=Off -DENABLE_TESTING_ADDONS=Off -DENABLE_ENCHANT=Off \
|
||||
-DENABLE_DOC=Off -DENABLE_XDGAUTOSTART=Off -DENABLE_EMOJI=Off
|
||||
|
||||
echo ">> building"
|
||||
ninja -C "$work/cmake" m3ui src/ui/m3ui/m3ui.conf
|
||||
echo ">> done, run scripts/install.sh"
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
# Installs the addon, the themes and an example config into your home
|
||||
# directory. Nothing outside $HOME is touched and the stock classicui stays
|
||||
# available.
|
||||
set -euo pipefail
|
||||
|
||||
root=$(cd "$(dirname "$0")/.." && pwd)
|
||||
work=${WORK_DIR:-$root/build}
|
||||
prefix=${PREFIX:-$HOME/.local/m3ui}
|
||||
data=${XDG_DATA_HOME:-$HOME/.local/share}/fcitx5
|
||||
config=${XDG_CONFIG_HOME:-$HOME/.config}/fcitx5
|
||||
|
||||
# Going through cmake's install step strips the build tree rpath, otherwise the
|
||||
# addon would load a second copy of libFcitx5Core from the build directory.
|
||||
cmake -DCMAKE_INSTALL_PREFIX="$prefix" -P "$work/cmake/src/ui/m3ui/cmake_install.cmake"
|
||||
|
||||
mkdir -p "$data/addon" "$data/themes" "$config/conf"
|
||||
cp "$prefix/share/fcitx5/addon/m3ui.conf" "$data/addon/m3ui.conf"
|
||||
cp -r "$root"/themes/* "$data/themes/"
|
||||
if [ ! -e "$config/conf/m3ui.conf" ]; then
|
||||
cp "$root/config/m3ui.conf.example" "$config/conf/m3ui.conf"
|
||||
fi
|
||||
|
||||
addondir=$(dirname "$(find "$prefix" -name libm3ui.so | head -1)")
|
||||
cat <<MSG
|
||||
Installed. Try it with:
|
||||
|
||||
FCITX_ADDON_DIRS=$addondir:\$(pkg-config --variable=addondir Fcitx5Core 2>/dev/null || echo /usr/lib64/fcitx5) fcitx5 -r -u m3ui
|
||||
|
||||
Go back to the stock interface with: fcitx5 -r
|
||||
MSG
|
||||
@@ -0,0 +1,57 @@
|
||||
if (ENABLE_X11 OR WAYLAND_FOUND)
|
||||
|
||||
set(CLASSICUI_SRCS)
|
||||
set(CLASSICUI_LIBS)
|
||||
if (WAYLAND_FOUND)
|
||||
set(CLASSICUI_SRCS ${CLASSICUI_SRCS}
|
||||
waylandui.cpp waylandwindow.cpp waylandshmwindow.cpp
|
||||
waylandpointer.cpp buffer.cpp waylandinputwindow.cpp
|
||||
waylandcursor.cpp waylandcursortheme.cpp)
|
||||
set(CLASSICUI_LIBS ${CLASSICUI_LIBS}
|
||||
Fcitx5::Module::Wayland Fcitx5::Module::WaylandIM Wayland::Client Fcitx5::Wayland::Core
|
||||
Wayland::Cursor
|
||||
Fcitx5::Wayland::InputMethod Fcitx5::Wayland::InputMethodV2
|
||||
Fcitx5::Wayland::ExtBackgroundEffect
|
||||
Fcitx5::Wayland::FractionalScale Fcitx5::Wayland::Viewporter
|
||||
Fcitx5::Wayland::CursorShape)
|
||||
else()
|
||||
endif()
|
||||
|
||||
if (ENABLE_X11)
|
||||
set(CLASSICUI_SRCS ${CLASSICUI_SRCS}
|
||||
xcbui.cpp xcbwindow.cpp xcbtraywindow.cpp xcbinputwindow.cpp xcbmenu.cpp)
|
||||
set(CLASSICUI_LIBS ${CLASSICUI_LIBS} PkgConfig::CairoXCB Fcitx5::Module::XCB
|
||||
XCB::AUX XCB::ICCCM XCB::XINERAMA XCB::RANDR XCB::EWMH)
|
||||
endif()
|
||||
|
||||
if (ENABLE_DBUS)
|
||||
set(CLASSICUI_SRCS ${CLASSICUI_SRCS} portalsettingmonitor.cpp)
|
||||
set(CLASSICUI_LIBS ${CLASSICUI_LIBS} Fcitx5::Module::DBus)
|
||||
endif()
|
||||
|
||||
add_fcitx5_addon(m3ui
|
||||
classicui.cpp window.cpp theme.cpp inputwindow.cpp plasmathemewatchdog.cpp colorhelper.cpp ${CLASSICUI_SRCS}
|
||||
)
|
||||
|
||||
if (ENABLE_X11)
|
||||
target_compile_definitions(m3ui PRIVATE -DFCITX_XCB_EWMH)
|
||||
endif()
|
||||
|
||||
target_link_libraries(m3ui
|
||||
Fcitx5::Core
|
||||
PkgConfig::Cairo PkgConfig::Pango
|
||||
PkgConfig::GdkPixbuf PkgConfig::Rsvg PkgConfig::GioUnix
|
||||
Fcitx5::Module::NotificationItem
|
||||
${YOGA_TARGET}
|
||||
${CLASSICUI_LIBS}
|
||||
)
|
||||
|
||||
install(TARGETS m3ui DESTINATION "${FCITX_INSTALL_ADDONDIR}")
|
||||
configure_file(classicui.conf.in.in m3ui.conf.in @ONLY)
|
||||
fcitx5_translate_desktop_file(${CMAKE_CURRENT_BINARY_DIR}/m3ui.conf.in m3ui.conf)
|
||||
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/m3ui.conf" DESTINATION "${FCITX_INSTALL_PKGDATADIR}/addon"
|
||||
COMPONENT config)
|
||||
|
||||
endif()
|
||||
|
||||
fcitx5_export_module(M3UI TARGET m3ui BUILD_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}" HEADERS classicui_public.h)
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2017-2020 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#include "buffer.h"
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#include <cerrno>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cairo.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <wayland-client.h>
|
||||
#include "fcitx-utils/environ.h"
|
||||
#include "fcitx-utils/fs.h"
|
||||
#include "fcitx-utils/stringutils.h"
|
||||
#include "fcitx-utils/unixfd.h"
|
||||
#include "wl_buffer.h"
|
||||
#include "wl_callback.h"
|
||||
#include "wl_shm.h"
|
||||
#include "wl_shm_pool.h"
|
||||
#include "wl_surface.h"
|
||||
|
||||
namespace fcitx::wayland {
|
||||
|
||||
#define RETRY_ON_EINTR(...) \
|
||||
do { \
|
||||
__VA_ARGS__; \
|
||||
} while (ret < 0 && errno == EINTR)
|
||||
|
||||
UnixFD openShm() {
|
||||
int ret;
|
||||
// We support multiple different methods, memfd / shm_open on BSD /
|
||||
// O_TMPFILE. While linux has shm_open, it doesn't have SHM_ANON extension
|
||||
// like bsd. Since memfd is introduced in 3.17, which is already quite old,
|
||||
// don't bother to add another implementation for Linux, since we need to
|
||||
// fallback to file as a final resolution.
|
||||
#if defined(__NR_memfd_create)
|
||||
do {
|
||||
static bool hasMemfdCreate = true;
|
||||
if (!hasMemfdCreate) {
|
||||
break;
|
||||
}
|
||||
int options = MFD_CLOEXEC;
|
||||
#if defined(MFD_ALLOW_SEALING)
|
||||
options |= MFD_ALLOW_SEALING;
|
||||
#endif
|
||||
RETRY_ON_EINTR(
|
||||
ret = syscall(__NR_memfd_create, "fcitx-wayland-shm", options));
|
||||
|
||||
if (ret < 0 && errno == ENOSYS) {
|
||||
hasMemfdCreate = false;
|
||||
break;
|
||||
}
|
||||
#if defined(F_ADD_SEALS) && defined(F_SEAL_SHRINK)
|
||||
if (ret >= 0) {
|
||||
fcntl(ret, F_ADD_SEALS, F_SEAL_SHRINK);
|
||||
return UnixFD::own(ret);
|
||||
}
|
||||
#endif
|
||||
} while (0);
|
||||
#endif
|
||||
|
||||
// Try shm_open(SHM_ANON) on BSD.
|
||||
#if defined(__FreeBSD__)
|
||||
RETRY_ON_EINTR(
|
||||
ret = shm_open(SHM_ANON, O_CREAT | O_EXCL | O_RDWR | O_CLOEXEC, 0600));
|
||||
if (ret >= 0) {
|
||||
return UnixFD::own(ret);
|
||||
}
|
||||
#endif
|
||||
|
||||
auto path = getEnvironment("XDG_RUNTIME_DIR");
|
||||
if (!path) {
|
||||
throw std::runtime_error("XDG_RUNTIME_DIR is not set");
|
||||
}
|
||||
|
||||
// Prefer O_TMPFILE over mkstemp.
|
||||
// The value check is to ensure it has the right value.
|
||||
// It is said that some old glibc may have problem.
|
||||
#if defined(O_TMPFILE) && (O_TMPFILE & O_DIRECTORY) == O_DIRECTORY
|
||||
do {
|
||||
std::string pathStr = fs::cleanPath(*path);
|
||||
RETRY_ON_EINTR(ret =
|
||||
open(pathStr.data(),
|
||||
O_TMPFILE | O_CLOEXEC | O_EXCL | O_RDWR, 0600));
|
||||
if (errno == EOPNOTSUPP || errno == EISDIR) {
|
||||
break;
|
||||
}
|
||||
if (ret >= 0) {
|
||||
return UnixFD::own(ret);
|
||||
}
|
||||
return {};
|
||||
} while (0);
|
||||
#endif
|
||||
|
||||
auto filename = std::filesystem::path(*path) / "fcitx-wayland-shm-XXXXXX";
|
||||
std::string v = filename.string();
|
||||
RETRY_ON_EINTR(ret = mkstemp(v.data()));
|
||||
|
||||
if (ret >= 0) {
|
||||
unlink(v.data());
|
||||
if (int flags = fcntl(ret, F_GETFD); flags != -1) {
|
||||
fcntl(ret, F_SETFD, flags | FD_CLOEXEC);
|
||||
}
|
||||
return UnixFD::own(ret);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
Buffer::Buffer(WlShm *shm, uint32_t width, uint32_t height,
|
||||
wl_shm_format format)
|
||||
: width_(width), height_(height) {
|
||||
uint32_t stride = width * 4;
|
||||
uint32_t alloc = stride * height;
|
||||
UnixFD fd = openShm();
|
||||
if (!fd.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (posix_fallocate(fd.fd(), 0, alloc) != 0) {
|
||||
return;
|
||||
}
|
||||
uint8_t *data = (uint8_t *)mmap(nullptr, alloc, PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED, fd.fd(), 0);
|
||||
|
||||
if (data == static_cast<uint8_t *>(MAP_FAILED)) {
|
||||
return;
|
||||
}
|
||||
|
||||
data_ = data;
|
||||
dataSize_ = alloc;
|
||||
|
||||
pool_.reset(shm->createPool(fd.fd(), alloc));
|
||||
buffer_.reset(pool_->createBuffer(0, width, height, stride, format));
|
||||
buffer_->release().connect([this]() { busy_ = false; });
|
||||
|
||||
surface_.reset(cairo_image_surface_create_for_data(
|
||||
data, CAIRO_FORMAT_ARGB32, width, height, stride));
|
||||
}
|
||||
|
||||
Buffer::~Buffer() {
|
||||
callback_.reset();
|
||||
surface_.reset();
|
||||
buffer_.reset();
|
||||
pool_.reset();
|
||||
if (data_) {
|
||||
munmap(data_, dataSize_);
|
||||
}
|
||||
}
|
||||
|
||||
bool Buffer::attachToSurface(WlSurface *surface, int scale) {
|
||||
if (busy_) {
|
||||
return false;
|
||||
}
|
||||
busy_ = true;
|
||||
callback_.reset(surface->frame());
|
||||
callback_->done().connect([this](uint32_t) {
|
||||
// CLASSICUI_DEBUG() << "Shm window rendered. " << this;
|
||||
// Need to ensure buffer won't be deleted.
|
||||
busy_ = false;
|
||||
rendered_();
|
||||
callback_.reset();
|
||||
});
|
||||
|
||||
surface->attach(buffer(), 0, 0);
|
||||
surface->setBufferScale(scale);
|
||||
surface->damage(0, 0, width_, height_);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace fcitx::wayland
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2017-2017 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#ifndef _FCITX_WAYLAND_CORE_BUFFER_H_
|
||||
#define _FCITX_WAYLAND_CORE_BUFFER_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <cairo.h>
|
||||
#include <wayland-client.h>
|
||||
#include "fcitx-utils/misc.h"
|
||||
#include "fcitx-utils/signals.h"
|
||||
|
||||
namespace fcitx {
|
||||
namespace wayland {
|
||||
|
||||
class WlShm;
|
||||
class WlShmPool;
|
||||
class WlBuffer;
|
||||
class WlCallback;
|
||||
class WlSurface;
|
||||
|
||||
class Buffer {
|
||||
public:
|
||||
Buffer(WlShm *shm, uint32_t width, uint32_t height, wl_shm_format format);
|
||||
~Buffer();
|
||||
|
||||
bool busy() const { return busy_; }
|
||||
uint32_t width() const { return width_; }
|
||||
uint32_t height() const { return height_; }
|
||||
cairo_surface_t *cairoSurface() const { return surface_.get(); }
|
||||
WlBuffer *buffer() const { return buffer_.get(); }
|
||||
|
||||
bool attachToSurface(WlSurface *surface, int scale);
|
||||
|
||||
auto &rendered() { return rendered_; }
|
||||
|
||||
private:
|
||||
uint8_t *data_ = nullptr;
|
||||
size_t dataSize_ = 0;
|
||||
Signal<void()> rendered_;
|
||||
std::unique_ptr<WlShmPool> pool_;
|
||||
std::unique_ptr<WlBuffer> buffer_;
|
||||
std::unique_ptr<WlCallback> callback_;
|
||||
UniqueCPtr<cairo_surface_t, cairo_surface_destroy> surface_;
|
||||
bool busy_ = false;
|
||||
uint32_t width_, height_;
|
||||
};
|
||||
} // namespace wayland
|
||||
} // namespace fcitx
|
||||
|
||||
#endif // _FCITX_WAYLAND_CORE_BUFFER_H_
|
||||
@@ -0,0 +1,18 @@
|
||||
[Addon]
|
||||
Name=Material 3 User Interface
|
||||
Type=@FCITX_ADDON_TYPE@
|
||||
Library=libm3ui
|
||||
Category=UI
|
||||
Version=@PROJECT_VERSION@
|
||||
OnDemand=True
|
||||
Configurable=True
|
||||
|
||||
[Addon/Dependencies]
|
||||
0=core:@PROJECT_VERSION@
|
||||
|
||||
[Addon/OptionalDependencies]
|
||||
0=xcb
|
||||
1=wayland
|
||||
2=notificationitem
|
||||
3=dbus
|
||||
4=waylandim
|
||||
@@ -0,0 +1,542 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2016-2016 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
|
||||
#include "classicui.h"
|
||||
#include <fcntl.h>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <cairo.h>
|
||||
#include "fcitx-config/iniparser.h"
|
||||
#include "fcitx-config/rawconfig.h"
|
||||
#include "fcitx-utils/color.h"
|
||||
#include "fcitx-utils/dbus/message_details.h"
|
||||
#include "fcitx-utils/eventloopinterface.h"
|
||||
#include "fcitx-utils/fs.h"
|
||||
#include "fcitx-utils/i18n.h"
|
||||
#include "fcitx-utils/log.h"
|
||||
#include "fcitx-utils/misc.h"
|
||||
#include "fcitx-utils/misc_p.h"
|
||||
#include "fcitx-utils/standardpaths.h"
|
||||
#include "fcitx/addonfactory.h"
|
||||
#include "fcitx/addoninstance.h"
|
||||
#include "fcitx/event.h"
|
||||
#include "fcitx/inputcontext.h"
|
||||
#include "fcitx/inputcontextmanager.h"
|
||||
#include "fcitx/instance.h"
|
||||
#include "fcitx/userinterface.h"
|
||||
#include "common.h"
|
||||
#include "notificationitem_public.h"
|
||||
#include "plasmathemewatchdog.h"
|
||||
#include "theme.h"
|
||||
#ifdef ENABLE_X11
|
||||
#include <xcb/xcb.h>
|
||||
#include "xcb_public.h"
|
||||
#include "xcbui.h"
|
||||
#endif
|
||||
#ifdef WAYLAND_FOUND
|
||||
#include "wayland_public.h"
|
||||
#include "waylandui.h"
|
||||
#endif
|
||||
#ifdef ENABLE_DBUS
|
||||
#include "fcitx-utils/dbus/variant.h"
|
||||
#include "dbus_public.h"
|
||||
#include "portalsettingmonitor.h"
|
||||
#endif
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
FCITX_DEFINE_LOG_CATEGORY(m3ui_logcategory, "m3ui");
|
||||
|
||||
using AccentColorDBusType = FCITX_STRING_TO_DBUS_TYPE("(ddd)");
|
||||
|
||||
M3UI::M3UI(Instance *instance) : instance_(instance) {
|
||||
#ifdef ENABLE_DBUS
|
||||
if (auto *dbusAddon = dbus()) {
|
||||
dbus::VariantTypeRegistry::defaultRegistry()
|
||||
.registerType<AccentColorDBusType>();
|
||||
settingMonitor_ = std::make_unique<PortalSettingMonitor>(
|
||||
*dbusAddon->call<IDBusModule::bus>());
|
||||
}
|
||||
#endif
|
||||
|
||||
reloadConfig();
|
||||
|
||||
#ifdef ENABLE_X11
|
||||
if (auto *xcbAddon = xcb()) {
|
||||
xcbCreatedCallback_ =
|
||||
xcbAddon->call<IXCBModule::addConnectionCreatedCallback>(
|
||||
[this](const std::string &name, xcb_connection_t *conn,
|
||||
int screen, FocusGroup *) {
|
||||
auto xcbui =
|
||||
std::make_unique<XCBUI>(this, name, conn, screen);
|
||||
uis_[xcbui->name()] = std::move(xcbui);
|
||||
CLASSICUI_INFO()
|
||||
<< "Created classicui for x11 display:" << name;
|
||||
});
|
||||
xcbClosedCallback_ =
|
||||
xcbAddon->call<IXCBModule::addConnectionClosedCallback>(
|
||||
[this](const std::string &name, xcb_connection_t *) {
|
||||
uis_.erase("x11:" + name);
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef WAYLAND_FOUND
|
||||
if (auto *waylandAddon = wayland()) {
|
||||
waylandCreatedCallback_ =
|
||||
waylandAddon->call<IWaylandModule::addConnectionCreatedCallback>(
|
||||
[this](const std::string &name, wl_display *display,
|
||||
FocusGroup *) {
|
||||
try {
|
||||
auto waylandui =
|
||||
std::make_unique<WaylandUI>(this, name, display);
|
||||
uis_[waylandui->name()] = std::move(waylandui);
|
||||
CLASSICUI_INFO()
|
||||
<< "Created classicui for wayland display:" << name;
|
||||
} catch (const std::runtime_error &) {
|
||||
}
|
||||
});
|
||||
waylandClosedCallback_ =
|
||||
waylandAddon->call<IWaylandModule::addConnectionClosedCallback>(
|
||||
[this](const std::string &name, wl_display *) {
|
||||
uis_.erase("wayland:" + name);
|
||||
});
|
||||
}
|
||||
#endif
|
||||
deferedReloadTheme_ =
|
||||
instance_->eventLoop().addDeferEvent([this](EventSource *) {
|
||||
reloadTheme();
|
||||
return true;
|
||||
});
|
||||
deferedReloadTheme_->setEnabled(false);
|
||||
|
||||
// Since kimpanel may call classicui
|
||||
persistentEventHandlers_.emplace_back(instance_->watchEvent(
|
||||
EventType::FocusGroupFocusChanged, EventWatcherPhase::Default,
|
||||
[this](Event &event) {
|
||||
auto &focusEvent =
|
||||
static_cast<FocusGroupFocusChangedEvent &>(event);
|
||||
if (!focusEvent.newFocus()) {
|
||||
if (auto *ui = uiForDisplay(focusEvent.group()->display())) {
|
||||
ui->update(UserInterfaceComponent::InputPanel, nullptr);
|
||||
}
|
||||
}
|
||||
}));
|
||||
persistentEventHandlers_.emplace_back(instance_->watchEvent(
|
||||
EventType::UIChanged, EventWatcherPhase::Default, [this](Event &) {
|
||||
if (instance_->currentUI() == "kimpanel") {
|
||||
deferedReloadTheme_->setOneShot();
|
||||
} else if (instance_->currentUI() == "m3ui") {
|
||||
deferedReloadTheme_->setOneShot();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
M3UI::~M3UI() {}
|
||||
|
||||
void M3UI::reloadConfig() {
|
||||
readAsIni(config_, "conf/m3ui.conf");
|
||||
reloadTheme();
|
||||
}
|
||||
|
||||
void M3UI::reloadTheme() {
|
||||
#ifdef ENABLE_DBUS
|
||||
auto parseMessage = [this](const dbus::Variant &variant) {
|
||||
if (variant.signature() == "u") {
|
||||
auto color = variant.dataAs<uint32_t>();
|
||||
auto oldIsDark = isDark_;
|
||||
isDark_ = (color == 1);
|
||||
if (oldIsDark != isDark_) {
|
||||
CLASSICUI_DEBUG() << "XDG Portal AppearanceChanged "
|
||||
"isDark"
|
||||
<< isDark_;
|
||||
deferedReloadTheme_->setOneShot();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (dbus()) {
|
||||
if (*config_.useDarkTheme) {
|
||||
if (!darkModeEntry_ && settingMonitor_) {
|
||||
darkModeEntry_ = settingMonitor_->watch(
|
||||
"org.freedesktop.appearance", "color-scheme", parseMessage);
|
||||
}
|
||||
} else {
|
||||
darkModeEntry_.reset();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
bool hasPlasmaTheme =
|
||||
instance_->currentUI() == "kimpanel" ||
|
||||
(*config_.theme == PlasmaThemeName) ||
|
||||
(*config_.useDarkTheme && *config_.themeDark == PlasmaThemeName);
|
||||
|
||||
if (hasPlasmaTheme) {
|
||||
if (!plasmaThemeWatchdog_ && PlasmaThemeWatchdog::isAvailable()) {
|
||||
try {
|
||||
plasmaThemeWatchdog_ = std::make_unique<PlasmaThemeWatchdog>(
|
||||
&instance_->eventLoop(), [this]() {
|
||||
CLASSICUI_DEBUG() << "Reload plasma theme";
|
||||
reloadTheme();
|
||||
});
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
plasmaThemeWatchdog_.reset();
|
||||
}
|
||||
|
||||
std::string_view themeName = (*config_.useDarkTheme && isDark_)
|
||||
? *config_.themeDark
|
||||
: *config_.theme;
|
||||
|
||||
if (instance_->currentUI() == "kimpanel") {
|
||||
if (plasmaThemeWatchdog_) {
|
||||
themeName = "plasma";
|
||||
}
|
||||
}
|
||||
|
||||
theme_.load(themeName);
|
||||
|
||||
#ifdef ENABLE_DBUS
|
||||
auto parseAccentColor = [this](const dbus::Variant &variant) {
|
||||
if (variant.signature() == "(ddd)") {
|
||||
auto dbuscolor = variant.dataAs<AccentColorDBusType>();
|
||||
Color color;
|
||||
color.setAlphaF(1);
|
||||
color.setRedF(std::get<0>(dbuscolor));
|
||||
color.setGreenF(std::get<1>(dbuscolor));
|
||||
color.setBlueF(std::get<2>(dbuscolor));
|
||||
if (!accentColor_ || *accentColor_ != color) {
|
||||
accentColor_ = color;
|
||||
CLASSICUI_DEBUG() << "XDG Portal AccentColor changed "
|
||||
"color: "
|
||||
<< accentColor_;
|
||||
deferedReloadTheme_->setOneShot();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (dbus()) {
|
||||
if (*config_.useAccentColor) {
|
||||
if (!accentColorEntry_ && settingMonitor_) {
|
||||
accentColorEntry_ =
|
||||
settingMonitor_->watch("org.freedesktop.appearance",
|
||||
"accent-color", parseAccentColor);
|
||||
}
|
||||
} else {
|
||||
accentColorEntry_.reset();
|
||||
accentColor_ = std::nullopt;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
theme_.populateColor(accentColor_);
|
||||
}
|
||||
|
||||
void M3UI::suspend() {
|
||||
suspended_ = true;
|
||||
for (auto &p : uis_) {
|
||||
p.second->suspend();
|
||||
}
|
||||
|
||||
if (auto *sni = notificationitem()) {
|
||||
sni->call<INotificationItem::disable>();
|
||||
}
|
||||
eventHandlers_.clear();
|
||||
}
|
||||
|
||||
const Configuration *M3UI::getConfig() const {
|
||||
auto themeDirs = StandardPaths::global().locate(
|
||||
StandardPathsType::PkgData, "themes",
|
||||
[](const std::filesystem::path &path) {
|
||||
std::error_code ec;
|
||||
return std::filesystem::is_directory(path, ec);
|
||||
});
|
||||
std::map<std::string, std::string, std::less<>> themes;
|
||||
for (const auto &[themeName, _] : themeDirs) {
|
||||
auto file = StandardPaths::global().open(
|
||||
StandardPathsType::PkgData,
|
||||
std::filesystem::path("themes") / themeName / "theme.conf",
|
||||
Theme::isSystemThemeName(themeName.string())
|
||||
? StandardPathsMode::System
|
||||
: StandardPathsMode::Default);
|
||||
if (file.fd() < 0) {
|
||||
continue;
|
||||
}
|
||||
RawConfig config;
|
||||
readFromIni(config, file.fd());
|
||||
|
||||
ThemeConfig themeConfig;
|
||||
themeConfig.load(config);
|
||||
if (!themeConfig.metadata.value()
|
||||
.name.value()
|
||||
.defaultString()
|
||||
.empty()) {
|
||||
themes[themeName] =
|
||||
themeConfig.metadata.value().name.value().match();
|
||||
}
|
||||
}
|
||||
|
||||
bool plasmaTheme = false;
|
||||
if (PlasmaThemeWatchdog::isAvailable()) {
|
||||
if (auto iter = themes.find(PlasmaThemeName); iter != themes.end()) {
|
||||
themes.erase(iter);
|
||||
}
|
||||
themes.emplace(PlasmaThemeName, _("KDE Plasma (Experimental)"));
|
||||
plasmaTheme = true;
|
||||
}
|
||||
|
||||
config_.theme.annotation().setThemes({themes.begin(), themes.end()},
|
||||
plasmaTheme);
|
||||
config_.themeDark.annotation().setThemes({themes.begin(), themes.end()},
|
||||
plasmaTheme);
|
||||
return &config_;
|
||||
}
|
||||
|
||||
UIInterface *M3UI::uiForEvent(Event &event) {
|
||||
if (suspended_) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!event.isInputContextEvent()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto &icEvent = static_cast<InputContextEvent &>(event);
|
||||
return uiForInputContext(icEvent.inputContext());
|
||||
}
|
||||
|
||||
UIInterface *M3UI::uiForInputContext(InputContext *inputContext) {
|
||||
if (suspended_ || !inputContext) {
|
||||
return nullptr;
|
||||
}
|
||||
if (!inputContext->hasFocus()) {
|
||||
return nullptr;
|
||||
}
|
||||
return uiForDisplay(inputContext->display());
|
||||
}
|
||||
|
||||
UIInterface *M3UI::uiForDisplay(const std::string &display) {
|
||||
auto iter = uis_.find(display);
|
||||
if (iter == uis_.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
return iter->second.get();
|
||||
}
|
||||
|
||||
void M3UI::resume() {
|
||||
CLASSICUI_DEBUG() << "Resume M3UI";
|
||||
suspended_ = false;
|
||||
for (auto &p : uis_) {
|
||||
p.second->resume();
|
||||
}
|
||||
|
||||
if (auto *sni = notificationitem()) {
|
||||
if (!sniHandler_) {
|
||||
sniHandler_ =
|
||||
sni->call<INotificationItem::watch>([this](bool enable) {
|
||||
for (auto &p : uis_) {
|
||||
p.second->setEnableTray(!enable);
|
||||
}
|
||||
});
|
||||
}
|
||||
sni->call<INotificationItem::enable>();
|
||||
// Delay 1 sec to enable xembed tray icon.
|
||||
deferedEnableTray_ = instance_->eventLoop().addTimeEvent(
|
||||
CLOCK_MONOTONIC, now(CLOCK_MONOTONIC) + 1000000, 0,
|
||||
[this](EventSource *, uint64_t) {
|
||||
// If we are now suspended, just return.
|
||||
if (suspended()) {
|
||||
return true;
|
||||
}
|
||||
if (auto *sni = notificationitem()) {
|
||||
auto registered =
|
||||
sni->call<INotificationItem::registered>();
|
||||
for (auto &p : uis_) {
|
||||
p.second->setEnableTray(!registered);
|
||||
}
|
||||
}
|
||||
deferedEnableTray_.reset();
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
for (auto &p : uis_) {
|
||||
p.second->setEnableTray(true);
|
||||
}
|
||||
}
|
||||
|
||||
eventHandlers_.emplace_back(instance_->watchEvent(
|
||||
EventType::InputContextCursorRectChanged, EventWatcherPhase::Default,
|
||||
[this](Event &event) {
|
||||
if (auto *ui = uiForEvent(event)) {
|
||||
auto &icEvent = static_cast<InputContextEvent &>(event);
|
||||
ui->updateCursor(icEvent.inputContext());
|
||||
}
|
||||
}));
|
||||
eventHandlers_.emplace_back(instance_->watchEvent(
|
||||
EventType::InputContextFocusIn, EventWatcherPhase::Default,
|
||||
[this](Event &event) {
|
||||
if (auto *ui = uiForEvent(event)) {
|
||||
auto &icEvent = static_cast<InputContextEvent &>(event);
|
||||
ui->updateCursor(icEvent.inputContext());
|
||||
ui->updateCurrentInputMethod(icEvent.inputContext());
|
||||
}
|
||||
}));
|
||||
eventHandlers_.emplace_back(instance_->watchEvent(
|
||||
EventType::InputContextSwitchInputMethod, EventWatcherPhase::Default,
|
||||
[this](Event &event) {
|
||||
if (auto *ui = uiForEvent(event)) {
|
||||
auto &icEvent = static_cast<InputContextEvent &>(event);
|
||||
ui->updateCurrentInputMethod(icEvent.inputContext());
|
||||
}
|
||||
}));
|
||||
eventHandlers_.emplace_back(instance_->watchEvent(
|
||||
EventType::InputMethodGroupChanged, EventWatcherPhase::Default,
|
||||
[this](Event &) {
|
||||
instance_->inputContextManager().foreachFocused(
|
||||
[this](InputContext *ic) {
|
||||
if (auto *ui = uiForInputContext(ic)) {
|
||||
ui->updateCurrentInputMethod(ic);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}));
|
||||
eventHandlers_.emplace_back(instance_->watchEvent(
|
||||
EventType::FocusGroupFocusChanged, EventWatcherPhase::Default,
|
||||
[this](Event &event) {
|
||||
auto &focusEvent =
|
||||
static_cast<FocusGroupFocusChangedEvent &>(event);
|
||||
if (!focusEvent.newFocus()) {
|
||||
if (auto *ui = uiForDisplay(focusEvent.group()->display())) {
|
||||
ui->update(UserInterfaceComponent::InputPanel, nullptr);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
void M3UI::update(UserInterfaceComponent component,
|
||||
InputContext *inputContext) {
|
||||
UIInterface *ui = nullptr;
|
||||
if (inputContext->display().starts_with("wayland:") &&
|
||||
!inputContext->frontendName().starts_with("wayland")) {
|
||||
// If display is wayland, but frontend is not, then we can only do X11
|
||||
// for now, though position is wrong. We don't know which is xwayland
|
||||
// unfortunately, hopefully main display is X wayland.
|
||||
// The position will be wrong anyway.
|
||||
#ifdef ENABLE_X11
|
||||
if (auto *xcbAddon = xcb()) {
|
||||
auto mainX11Display = xcbAddon->call<IXCBModule::mainDisplay>();
|
||||
if (!mainX11Display.empty()) {
|
||||
if (auto *uiPtr = findValue(uis_, "x11:" + mainX11Display)) {
|
||||
ui = uiPtr->get();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
if (auto *uiPtr = findValue(uis_, inputContext->display())) {
|
||||
ui = uiPtr->get();
|
||||
}
|
||||
}
|
||||
CLASSICUI_DEBUG() << "Update component: " << static_cast<int>(component)
|
||||
<< " for IC program:" << inputContext->program()
|
||||
<< " frontend:" << inputContext->frontendName()
|
||||
<< " display:" << inputContext->display()
|
||||
<< " ui:" << (ui ? ui->name() : "(not available)");
|
||||
|
||||
if (ui) {
|
||||
ui->update(component, inputContext);
|
||||
if (component == UserInterfaceComponent::StatusArea) {
|
||||
ui->updateCurrentInputMethod(inputContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class M3UIFactory : public AddonFactory {
|
||||
public:
|
||||
AddonInstance *create(AddonManager *manager) override {
|
||||
return new M3UI(manager->instance());
|
||||
}
|
||||
};
|
||||
|
||||
const fcitx::Configuration *
|
||||
M3UI::getSubConfig(const std::string &path) const {
|
||||
if (!path.starts_with("theme/")) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto name = path.substr(6);
|
||||
if (!ThemeNameConstrain{}.check(name)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
subconfigTheme_.load(name);
|
||||
return &subconfigTheme_;
|
||||
}
|
||||
|
||||
void M3UI::setSubConfig(const std::string &path,
|
||||
const fcitx::RawConfig &config) {
|
||||
if (!path.starts_with("theme/")) {
|
||||
return;
|
||||
}
|
||||
auto name = path.substr(6);
|
||||
if (!ThemeNameConstrain{}.check(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Theme::isSystemThemeName(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto &theme = name == theme_.name() ? theme_ : subconfigTheme_;
|
||||
if (&theme == &subconfigTheme_) {
|
||||
// Fill the system value.
|
||||
getSubConfig(path);
|
||||
} else {
|
||||
deferedReloadTheme_->setOneShot();
|
||||
}
|
||||
theme.load(name, config);
|
||||
safeSaveAsIni(theme, StandardPathsType::PkgData,
|
||||
std::filesystem::path("themes") / name / "theme.conf");
|
||||
}
|
||||
|
||||
std::vector<unsigned char> M3UI::labelIcon(const std::string &label,
|
||||
unsigned int size) {
|
||||
std::vector<unsigned char> data;
|
||||
size_t stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, size);
|
||||
data.resize(stride * size);
|
||||
UniqueCPtr<cairo_surface_t, cairo_surface_destroy> image;
|
||||
image.reset(cairo_image_surface_create_for_data(
|
||||
data.data(), CAIRO_FORMAT_ARGB32, size, size, stride));
|
||||
ThemeImage::drawTextIcon(image.get(), label, size, config_);
|
||||
image.reset();
|
||||
return data;
|
||||
}
|
||||
|
||||
bool M3UI::preferTextIcon() const { return *config_.preferTextIcon; }
|
||||
|
||||
bool M3UI::showLayoutNameInIcon() const {
|
||||
return *config_.showLayoutNameInIcon;
|
||||
}
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
|
||||
FCITX_ADDON_FACTORY_V2(m3ui, fcitx::m3ui::M3UIFactory);
|
||||
@@ -0,0 +1,321 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2016-2016 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#ifndef _FCITX_UI_CLASSIC_CLASSICUI_H_
|
||||
#define _FCITX_UI_CLASSIC_CLASSICUI_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include "fcitx-config/configuration.h"
|
||||
#include "fcitx-config/iniparser.h"
|
||||
#include "fcitx-config/option.h"
|
||||
#include "fcitx-config/rawconfig.h"
|
||||
#include "fcitx-utils/color.h"
|
||||
#include "fcitx-utils/eventloopinterface.h"
|
||||
#include "fcitx-utils/handlertable.h"
|
||||
#include "fcitx-utils/i18n.h"
|
||||
#include "fcitx-utils/stringutils.h"
|
||||
#include "fcitx/addoninstance.h"
|
||||
#include "fcitx/addonmanager.h"
|
||||
#include "fcitx/event.h"
|
||||
#include "fcitx/instance.h"
|
||||
#include "fcitx/userinterface.h"
|
||||
#include "classicui_public.h"
|
||||
#include "config.h"
|
||||
#include "plasmathemewatchdog.h"
|
||||
#include "portalsettingmonitor.h"
|
||||
#include "theme.h"
|
||||
#ifdef ENABLE_X11
|
||||
#include "xcb_public.h"
|
||||
#endif
|
||||
#ifdef WAYLAND_FOUND
|
||||
#include "wayland_public.h"
|
||||
#endif
|
||||
#ifdef ENABLE_DBUS
|
||||
#endif
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
inline constexpr std::string_view PlasmaThemeName = "plasma";
|
||||
|
||||
class UIInterface {
|
||||
public:
|
||||
UIInterface(std::string name) : name_(std::move(name)) {}
|
||||
|
||||
const std::string &name() const { return name_; }
|
||||
|
||||
virtual ~UIInterface() {}
|
||||
virtual void update(UserInterfaceComponent component,
|
||||
InputContext *inputContext) = 0;
|
||||
virtual void updateCursor(InputContext * /*unused*/) {}
|
||||
virtual void updateCurrentInputMethod(InputContext * /*unused*/) {}
|
||||
virtual void suspend() = 0;
|
||||
virtual void resume() {}
|
||||
virtual void setEnableTray(bool) = 0;
|
||||
|
||||
private:
|
||||
const std::string name_;
|
||||
};
|
||||
|
||||
struct ThemeNameConstrain {
|
||||
using Type = std::string;
|
||||
|
||||
bool check(const std::string &value) const {
|
||||
const auto path = std::filesystem::path(value);
|
||||
return !value.empty() && path.is_relative() &&
|
||||
!path.has_parent_path() && path.filename() == path &&
|
||||
path.filename() != "." && path.filename() != "..";
|
||||
}
|
||||
|
||||
void dumpDescription(RawConfig & /*unused*/) const {}
|
||||
};
|
||||
|
||||
struct ThemeAnnotation : public EnumAnnotation {
|
||||
void setThemes(std::vector<std::pair<std::string, std::string>> themes,
|
||||
bool plasmaTheme) {
|
||||
themes_ = std::move(themes);
|
||||
plasmaTheme_ = plasmaTheme;
|
||||
}
|
||||
void dumpDescription(RawConfig &config) const {
|
||||
EnumAnnotation::dumpDescription(config);
|
||||
config.setValueByPath("LaunchSubConfig", "True");
|
||||
for (size_t i = 0; i < themes_.size(); i++) {
|
||||
config.setValueByPath("Enum/" + std::to_string(i),
|
||||
themes_[i].first);
|
||||
config.setValueByPath("EnumI18n/" + std::to_string(i),
|
||||
themes_[i].second);
|
||||
if (themes_[i].first != PlasmaThemeName &&
|
||||
!Theme::isSystemThemeName(themes_[i].first)) {
|
||||
config.setValueByPath(
|
||||
"SubConfigPath/" + std::to_string(i),
|
||||
stringutils::concat("fcitx://config/addon/m3ui/theme/",
|
||||
themes_[i].first));
|
||||
} else {
|
||||
config.setValueByPath("SubConfigPath/" + std::to_string(i), "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::pair<std::string, std::string>> themes_;
|
||||
bool plasmaTheme_ = false;
|
||||
};
|
||||
|
||||
struct MenuFontAnnotation : private FontAnnotation, private ToolTipAnnotation {
|
||||
MenuFontAnnotation()
|
||||
: ToolTipAnnotation(
|
||||
_("This is only effective when the tray icon is xembed.")) {}
|
||||
|
||||
bool skipDescription() { return false; }
|
||||
bool skipSave() { return false; }
|
||||
void dumpDescription(RawConfig &config) {
|
||||
FontAnnotation::dumpDescription(config);
|
||||
ToolTipAnnotation::dumpDescription(config);
|
||||
}
|
||||
};
|
||||
|
||||
FCITX_CONFIGURATION(
|
||||
ClassicUIConfig,
|
||||
Option<bool> verticalCandidateList{this, "Vertical Candidate List",
|
||||
_("Vertical Candidate List"), false};
|
||||
Option<bool> useWheelForPaging{
|
||||
this, "WheelForPaging", _("Use mouse wheel to go to prev or next page"),
|
||||
true};
|
||||
|
||||
OptionWithAnnotation<std::string, FontAnnotation> font{
|
||||
this, "Font", _("Font"), "Sans 10"};
|
||||
OptionWithAnnotation<std::string, MenuFontAnnotation> menuFont{
|
||||
this, "MenuFont", _("Menu Font"), "Sans 10"};
|
||||
OptionWithAnnotation<std::string, FontAnnotation> trayFont{
|
||||
this, "TrayFont", _("Tray Font"), "Sans Bold 10"};
|
||||
Option<Color> trayBorderColor{this, "TrayOutlineColor",
|
||||
_("Tray Label Outline Color"),
|
||||
Color("#000000ff")};
|
||||
Option<Color> trayTextColor{this, "TrayTextColor",
|
||||
_("Tray Label Text Color"), Color("#ffffffff")};
|
||||
Option<bool> preferTextIcon{this, "PreferTextIcon", _("Prefer Text Icon"),
|
||||
false};
|
||||
OptionWithAnnotation<bool, ToolTipAnnotation> showLayoutNameInIcon{
|
||||
this,
|
||||
"ShowLayoutNameInIcon",
|
||||
_("Show Layout Name In Icon"),
|
||||
true,
|
||||
{},
|
||||
{},
|
||||
{_("Show layout name in icon if there is more than one active layout. "
|
||||
"If prefer text icon is set to true, this option will be "
|
||||
"ignored.")}};
|
||||
OptionWithAnnotation<bool, ToolTipAnnotation>
|
||||
useInputMethodLanguageToDisplayText{
|
||||
this,
|
||||
"UseInputMethodLanguageToDisplayText",
|
||||
_("Use input method language to display text"),
|
||||
true,
|
||||
{},
|
||||
{},
|
||||
{_("For example, display character with Chinese variant when using "
|
||||
"Pinyin and Japanese variant when using Anthy. The font "
|
||||
"configuration needs to support this to use this feature.")}};
|
||||
Option<std::string, ThemeNameConstrain, DefaultMarshaller<std::string>,
|
||||
ThemeAnnotation>
|
||||
theme{this, "Theme", _("Theme"), "default"};
|
||||
Option<std::string, ThemeNameConstrain, DefaultMarshaller<std::string>,
|
||||
ThemeAnnotation>
|
||||
themeDark{this, "DarkTheme", _("Dark Theme"), "default-dark"};
|
||||
Option<bool> useDarkTheme{this, "UseDarkTheme",
|
||||
_("Follow system light/dark color scheme"),
|
||||
false};
|
||||
Option<bool> useAccentColor{
|
||||
this, "UseAccentColor",
|
||||
_("Follow system accent color if it is supported by theme and desktop"),
|
||||
true};
|
||||
OptionWithAnnotation<bool, ToolTipAnnotation> perScreenDPI{
|
||||
this,
|
||||
"PerScreenDPI",
|
||||
_("Use Per Screen DPI on X11"),
|
||||
false,
|
||||
{},
|
||||
{},
|
||||
{_("This option will be always disabled on XWayland.")}};
|
||||
Option<int, IntConstrain, DefaultMarshaller<int>, ToolTipAnnotation>
|
||||
forceWaylandDPI{
|
||||
this,
|
||||
"ForceWaylandDPI",
|
||||
_("Force font DPI on Wayland"),
|
||||
0,
|
||||
IntConstrain(0),
|
||||
{},
|
||||
{_("Normally Wayland uses 96 as font DPI in combination with the "
|
||||
"screen scale factor. This option allows you to override the "
|
||||
"font DPI. If the value is 0, it means this option is "
|
||||
"disabled.")}};
|
||||
|
||||
Option<bool> animations{this, "Animations", _("Enable animations"), true};
|
||||
Option<bool> bounce{this, "Bounce",
|
||||
_("Bouncy overshoot (off: critically damped)"), true};
|
||||
Option<int, IntConstrain> spatialStiffness{
|
||||
this, "SpatialStiffness",
|
||||
_("Spring stiffness for movement, higher is faster"), 2000,
|
||||
IntConstrain(50, 20000)};
|
||||
Option<int, IntConstrain> spatialDamping{
|
||||
this, "SpatialDamping",
|
||||
_("Spring damping ratio for movement in percent, lower bounces more"),
|
||||
62, IntConstrain(20, 100)};
|
||||
Option<int, IntConstrain> effectsStiffness{
|
||||
this, "EffectsStiffness",
|
||||
_("Spring stiffness for fading, higher is faster"), 3800,
|
||||
IntConstrain(50, 20000)};
|
||||
Option<int, IntConstrain> slideDistance{
|
||||
this, "SlideDistance", _("Slide-in distance (px)"), 22,
|
||||
IntConstrain(0, 200)};
|
||||
Option<int, IntConstrain> slideDirection{
|
||||
this, "SlideDirection",
|
||||
_("Slide-in direction (0: from left, 1: from right, 2: from below)"),
|
||||
0, IntConstrain(0, 2)};
|
||||
|
||||
OptionWithAnnotation<bool, ToolTipAnnotation> fractionalScale{
|
||||
this,
|
||||
"EnableFractionalScale",
|
||||
_("Enable fractional scale under Wayland"),
|
||||
true,
|
||||
{},
|
||||
{},
|
||||
{_("This option require support from wayland compositor.")}};);
|
||||
|
||||
class M3UI final : public UserInterface {
|
||||
public:
|
||||
M3UI(Instance *instance);
|
||||
~M3UI();
|
||||
|
||||
FCITX_ADDON_DEPENDENCY_LOADER(xcb, instance_->addonManager());
|
||||
FCITX_ADDON_DEPENDENCY_LOADER(wayland, instance_->addonManager());
|
||||
FCITX_ADDON_DEPENDENCY_LOADER(waylandim, instance_->addonManager());
|
||||
FCITX_ADDON_DEPENDENCY_LOADER(dbus, instance_->addonManager());
|
||||
Instance *instance() const { return instance_; }
|
||||
const Configuration *getConfig() const override;
|
||||
void setConfig(const RawConfig &config) override {
|
||||
config_.load(config, true);
|
||||
safeSaveAsIni(config_, "conf/m3ui.conf");
|
||||
reloadTheme();
|
||||
}
|
||||
const Configuration *getSubConfig(const std::string &path) const override;
|
||||
void setSubConfig(const std::string &path,
|
||||
const RawConfig &config) override;
|
||||
auto &config() const { return config_; }
|
||||
Theme &theme() { return theme_; }
|
||||
void suspend() override;
|
||||
void resume() override;
|
||||
bool suspended() const { return suspended_; }
|
||||
bool available() override { return true; }
|
||||
void update(UserInterfaceComponent component,
|
||||
InputContext *inputContext) override;
|
||||
void reloadConfig() override;
|
||||
|
||||
std::vector<unsigned char> labelIcon(const std::string &label,
|
||||
unsigned int size);
|
||||
bool preferTextIcon() const;
|
||||
bool showLayoutNameInIcon() const;
|
||||
|
||||
private:
|
||||
FCITX_ADDON_DEPENDENCY_LOADER(notificationitem, instance_->addonManager());
|
||||
FCITX_ADDON_EXPORT_FUNCTION(M3UI, labelIcon);
|
||||
FCITX_ADDON_EXPORT_FUNCTION(M3UI, preferTextIcon);
|
||||
FCITX_ADDON_EXPORT_FUNCTION(M3UI, showLayoutNameInIcon);
|
||||
|
||||
UIInterface *uiForEvent(Event &event);
|
||||
UIInterface *uiForInputContext(InputContext *inputContext);
|
||||
UIInterface *uiForDisplay(const std::string &display);
|
||||
void reloadTheme();
|
||||
|
||||
#ifdef ENABLE_X11
|
||||
std::unique_ptr<HandlerTableEntry<XCBConnectionCreated>>
|
||||
xcbCreatedCallback_;
|
||||
std::unique_ptr<HandlerTableEntry<XCBConnectionClosed>> xcbClosedCallback_;
|
||||
#endif
|
||||
|
||||
#ifdef WAYLAND_FOUND
|
||||
std::unique_ptr<HandlerTableEntry<WaylandConnectionCreated>>
|
||||
waylandCreatedCallback_;
|
||||
std::unique_ptr<HandlerTableEntry<WaylandConnectionClosed>>
|
||||
waylandClosedCallback_;
|
||||
#endif
|
||||
|
||||
std::unique_ptr<EventSource> deferedReloadTheme_;
|
||||
#ifdef ENABLE_DBUS
|
||||
std::unique_ptr<PortalSettingMonitor> settingMonitor_;
|
||||
std::unique_ptr<PortalSettingEntry> darkModeEntry_;
|
||||
std::unique_ptr<PortalSettingEntry> accentColorEntry_;
|
||||
#endif
|
||||
|
||||
std::vector<std::unique_ptr<HandlerTableEntry<EventHandler>>>
|
||||
eventHandlers_;
|
||||
std::vector<std::unique_ptr<HandlerTableEntry<EventHandler>>>
|
||||
persistentEventHandlers_;
|
||||
std::unique_ptr<HandlerTableEntryBase> sniHandler_;
|
||||
|
||||
std::unordered_map<std::string, std::unique_ptr<UIInterface>> uis_;
|
||||
|
||||
Instance *instance_;
|
||||
ClassicUIConfig config_;
|
||||
Theme theme_;
|
||||
mutable Theme subconfigTheme_;
|
||||
bool suspended_ = true;
|
||||
bool isDark_ = false;
|
||||
std::optional<Color> accentColor_ = std::nullopt;
|
||||
|
||||
std::unique_ptr<EventSource> deferedEnableTray_;
|
||||
std::unique_ptr<PlasmaThemeWatchdog> plasmaThemeWatchdog_;
|
||||
};
|
||||
} // namespace fcitx::m3ui
|
||||
|
||||
#endif // _FCITX_UI_CLASSIC_CLASSICUI_H_
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021~2021 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#ifndef _FCITX5_UI_CLASSIC_CLASSICUI_PUBLIC_H_
|
||||
#define _FCITX5_UI_CLASSIC_CLASSICUI_PUBLIC_H_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <fcitx-utils/metastring.h>
|
||||
#include <fcitx/addoninstance.h>
|
||||
|
||||
FCITX_ADDON_DECLARE_FUNCTION(
|
||||
M3UI, labelIcon,
|
||||
std::vector<unsigned char>(const std::string &label, unsigned int size));
|
||||
FCITX_ADDON_DECLARE_FUNCTION(M3UI, preferTextIcon, bool());
|
||||
FCITX_ADDON_DECLARE_FUNCTION(M3UI, showLayoutNameInIcon, bool());
|
||||
|
||||
#endif // _FCITX5_UI_CLASSIC_CLASSICUI_PUBLIC_H_
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2023 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#include "colorhelper.h"
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
#include "fcitx-utils/color.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
inline float normalize(float v) {
|
||||
return (v < 1.0 ? (v > 0.0 ? v : 0.0) : 1.0);
|
||||
}
|
||||
|
||||
float gamma(float n) { return std::pow(normalize(n), 2.2); }
|
||||
|
||||
float lumag(float r, float g, float b) {
|
||||
return r * 0.2126 + g * 0.7152 + b * 0.0722;
|
||||
}
|
||||
|
||||
float luma(const Color &c) {
|
||||
return lumag(gamma(c.redF()), gamma(c.greenF()), gamma(c.blueF()));
|
||||
}
|
||||
|
||||
std::pair<Color, Color> accentForeground(const Color &accent) {
|
||||
// light bg
|
||||
if (luma(accent) > 0.5) {
|
||||
return {Color(0, 0, 0), Color(51, 51, 51)};
|
||||
}
|
||||
return {Color(255, 255, 255), Color(204, 204, 204)};
|
||||
}
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2023 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#ifndef _FCITX_UI_CLASSIC_COLORHELPER_H_
|
||||
#define _FCITX_UI_CLASSIC_COLORHELPER_H_
|
||||
|
||||
#include <utility>
|
||||
#include "fcitx-utils/color.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
std::pair<Color, Color> accentForeground(const Color &accent);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020-2020 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#ifndef _FCITX5_UI_CLASSIC_COMMON_H_
|
||||
#define _FCITX5_UI_CLASSIC_COMMON_H_
|
||||
|
||||
#include <memory>
|
||||
#include <glib-object.h>
|
||||
#include <yoga/YGNode.h>
|
||||
#include "fcitx-utils/log.h"
|
||||
#include "fcitx-utils/misc.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
template <typename T>
|
||||
using GObjectUniquePtr = UniqueCPtr<T, g_object_unref>;
|
||||
|
||||
template <auto Getter>
|
||||
float absolute(YGNodeRef node) {
|
||||
float offset = 0.0F;
|
||||
while (node) {
|
||||
offset += Getter(node);
|
||||
node = YGNodeGetParent(node);
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
template <auto Getter, typename T, typename Deleter>
|
||||
float absolute(const std::unique_ptr<T, Deleter> &node) {
|
||||
return absolute<Getter>(node.get());
|
||||
}
|
||||
|
||||
FCITX_DECLARE_LOG_CATEGORY(m3ui_logcategory);
|
||||
#define CLASSICUI_DEBUG() \
|
||||
FCITX_LOGC(::fcitx::m3ui::m3ui_logcategory, Debug)
|
||||
#define CLASSICUI_ERROR() \
|
||||
FCITX_LOGC(::fcitx::m3ui::m3ui_logcategory, Error)
|
||||
#define CLASSICUI_INFO() \
|
||||
FCITX_LOGC(::fcitx::m3ui::m3ui_logcategory, Info)
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
|
||||
#endif // _FCITX5_UI_CLASSIC_COMMON_H_
|
||||
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2017-2017 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#ifndef _FCITX_UI_CLASSIC_INPUTWINDOW_H_
|
||||
#define _FCITX_UI_CLASSIC_INPUTWINDOW_H_
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <cairo.h>
|
||||
#include <pango/pango-attributes.h>
|
||||
#include <pango/pango-context.h>
|
||||
#include <pango/pango-font.h>
|
||||
#include <pango/pango-layout.h>
|
||||
#include <pango/pango-types.h>
|
||||
#include <pango/pango.h>
|
||||
#include <yoga/YGConfig.h>
|
||||
#include <yoga/YGNode.h>
|
||||
#include <yoga/Yoga.h>
|
||||
#include "fcitx-utils/event.h"
|
||||
#include "fcitx-utils/macros.h"
|
||||
#include "fcitx-utils/misc.h"
|
||||
#include "fcitx-utils/rect.h"
|
||||
#include "fcitx-utils/textformatflags.h"
|
||||
#include "fcitx-utils/trackableobject.h"
|
||||
#include "fcitx/candidateaction.h"
|
||||
#include "fcitx/candidatelist.h"
|
||||
#include "fcitx/inputcontext.h"
|
||||
#include "fcitx/text.h"
|
||||
#include "common.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
class M3UI;
|
||||
|
||||
using PangoAttrListUniquePtr = UniqueCPtr<PangoAttrList, pango_attr_list_unref>;
|
||||
|
||||
class MultilineLayout {
|
||||
public:
|
||||
MultilineLayout() = default;
|
||||
FCITX_INLINE_DEFINE_DEFAULT_DTOR_AND_MOVE(MultilineLayout);
|
||||
|
||||
void contextChanged() {
|
||||
for (const auto &layout : lines_) {
|
||||
pango_layout_context_changed(layout.get());
|
||||
}
|
||||
}
|
||||
void setFontDescription(const PangoFontDescription *fontDesc) {
|
||||
for (const auto &layout : lines_) {
|
||||
pango_layout_set_font_description(layout.get(), fontDesc);
|
||||
}
|
||||
}
|
||||
int fontHeight() const {
|
||||
if (lines_.empty()) {
|
||||
return 0;
|
||||
}
|
||||
auto *context = pango_layout_get_context(lines_[0].get());
|
||||
if (!context) {
|
||||
return 0;
|
||||
}
|
||||
const auto *fontDescription =
|
||||
pango_layout_get_font_description(lines_[0].get());
|
||||
if (!fontDescription) {
|
||||
fontDescription = pango_context_get_font_description(context);
|
||||
}
|
||||
if (!fontDescription) {
|
||||
return 0;
|
||||
}
|
||||
auto *metrics = pango_context_get_metrics(
|
||||
context, fontDescription, pango_context_get_language(context));
|
||||
int fontHeight = pango_font_metrics_get_ascent(metrics) +
|
||||
pango_font_metrics_get_descent(metrics);
|
||||
pango_font_metrics_unref(metrics);
|
||||
return PANGO_PIXELS(fontHeight);
|
||||
}
|
||||
int characterCount() const {
|
||||
int count = 0;
|
||||
for (const auto &layout : lines_) {
|
||||
count += pango_layout_get_character_count(layout.get());
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
int width() const;
|
||||
|
||||
int size() const { return lines_.size(); }
|
||||
void render(cairo_t *cr, int x, int y, bool highlight);
|
||||
|
||||
std::vector<GObjectUniquePtr<PangoLayout>> lines_;
|
||||
std::vector<PangoAttrListUniquePtr> attrLists_;
|
||||
std::vector<PangoAttrListUniquePtr> highlightAttrLists_;
|
||||
};
|
||||
|
||||
class InputWindow {
|
||||
public:
|
||||
InputWindow(M3UI *parent);
|
||||
~InputWindow() = default;
|
||||
std::pair<int, int> update(InputContext *inputContext);
|
||||
void paint(cairo_t *cr, double width, double height);
|
||||
void hide();
|
||||
bool visible() const { return visible_; }
|
||||
bool hover(int x, int y);
|
||||
void click(int x, int y);
|
||||
// Right click on a candidate opens the action menu (forget, pin, ...).
|
||||
void rightClick(int x, int y);
|
||||
void wheel(bool up);
|
||||
// Repaint the window using the current state, implemented by backends.
|
||||
virtual void repaint() {}
|
||||
|
||||
protected:
|
||||
enum class TextType { Label, Regular, Comment };
|
||||
|
||||
// Override the font DPI in cairo/pango context. If less-equal than zero, it
|
||||
// will restore to font map default dpi.
|
||||
void setFontDPI(int dpi);
|
||||
void resizeCandidates(size_t n);
|
||||
void appendText(std::string &s, PangoAttrList *attrList,
|
||||
PangoAttrList *highlightAttrList, const Text &text,
|
||||
TextType type);
|
||||
void insertAttr(PangoAttrList *attrList, TextFormatFlags format, int start,
|
||||
int end, bool highlight, TextType type) const;
|
||||
void setTextToLayout(
|
||||
InputContext *inputContext, PangoLayout *layout,
|
||||
PangoAttrListUniquePtr *attrList,
|
||||
PangoAttrListUniquePtr *highlightAttrList,
|
||||
std::initializer_list<std::reference_wrapper<const Text>> texts,
|
||||
TextType type = TextType::Regular);
|
||||
void setTextToMultilineLayout(InputContext *inputContext,
|
||||
MultilineLayout &layout, const Text &text,
|
||||
TextType type);
|
||||
int highlight() const;
|
||||
void updateYogaLayout();
|
||||
void renderYogaNode(cairo_t *cr, YGNodeRef node);
|
||||
|
||||
M3UI *parent_;
|
||||
GObjectUniquePtr<PangoFontMap> fontMap_;
|
||||
double fontMapDefaultDPI_ = 96.0;
|
||||
GObjectUniquePtr<PangoContext> context_;
|
||||
GObjectUniquePtr<PangoLayout> upperLayout_;
|
||||
GObjectUniquePtr<PangoLayout> lowerLayout_;
|
||||
struct CandidateLayout {
|
||||
MultilineLayout label;
|
||||
MultilineLayout text;
|
||||
MultilineLayout comment;
|
||||
};
|
||||
std::vector<CandidateLayout> candidateLayouts_;
|
||||
std::vector<Rect> candidateRegions_;
|
||||
TrackableObjectReference<InputContext> inputContext_;
|
||||
bool visible_ = false;
|
||||
int cursor_ = 0;
|
||||
size_t nCandidates_ = 0;
|
||||
bool hasPrev_ = false;
|
||||
bool hasNext_ = false;
|
||||
Rect prevRegion_;
|
||||
Rect nextRegion_;
|
||||
bool prevHovered_ = false;
|
||||
bool nextHovered_ = false;
|
||||
int candidateIndex_ = -1;
|
||||
CandidateLayoutHint layoutHint_ = CandidateLayoutHint::NotSet;
|
||||
int hoverIndex_ = -1;
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
struct RectF {
|
||||
double x = 0, y = 0, w = 0, h = 0;
|
||||
bool operator==(const RectF &o) const {
|
||||
return x == o.x && y == o.y && w == o.w && h == o.h;
|
||||
}
|
||||
};
|
||||
// Animation state, only used by themes with a Material surface color.
|
||||
bool materialStyle() const;
|
||||
bool animationsEnabled() const;
|
||||
void scheduleFrame();
|
||||
void startShowAnimation();
|
||||
Clock::time_point showStart_{};
|
||||
bool showAnimating_ = false;
|
||||
RectF highlightCurrent_;
|
||||
RectF highlightFrom_;
|
||||
RectF highlightTarget_;
|
||||
Clock::time_point highlightStart_{};
|
||||
bool highlightValid_ = false;
|
||||
bool highlightAnimating_ = false;
|
||||
std::string candidateSignature_;
|
||||
bool candidateListChanged_ = true;
|
||||
std::unique_ptr<EventSourceTime> frameTimer_;
|
||||
bool frameScheduled_ = false;
|
||||
|
||||
// Candidate action menu.
|
||||
void closeMenu();
|
||||
void paintMenu(cairo_t *cr, double width, double yTop, double fontHeight);
|
||||
std::pair<double, double> menuSize();
|
||||
bool menuOpen_ = false;
|
||||
int menuCandidate_ = -1;
|
||||
int menuHover_ = -1;
|
||||
std::vector<CandidateAction> menuActions_;
|
||||
std::vector<Rect> menuRegions_;
|
||||
double menuTop_ = 0;
|
||||
int menuItemHeight_ = 0;
|
||||
std::vector<std::string> menuLabels_;
|
||||
double menuTextWidth_ = 0;
|
||||
|
||||
private:
|
||||
std::pair<unsigned int, unsigned int> sizeHint();
|
||||
|
||||
using YGNodePtr = fcitx::UniqueCPtr<YGNode, YGNodeFree>;
|
||||
YGNodePtr rootNode_;
|
||||
YGNodePtr mainNode_;
|
||||
YGNodePtr upperNode_;
|
||||
YGNodePtr upperTextNode_;
|
||||
YGNodePtr lowerNode_;
|
||||
YGNodePtr auxDownNode_;
|
||||
YGNodePtr auxDownTextNode_;
|
||||
YGNodePtr candidatesNode_;
|
||||
|
||||
struct CandidateNode {
|
||||
YGNodePtr self{YGNodeNew()};
|
||||
YGNodePtr inner{YGNodeNew()};
|
||||
YGNodePtr label{YGNodeNew()};
|
||||
YGNodePtr text{YGNodeNew()};
|
||||
YGNodePtr comment{YGNodeNew()};
|
||||
};
|
||||
|
||||
std::vector<CandidateNode> candidateNodes_;
|
||||
YGNodePtr buttonNode_;
|
||||
};
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
|
||||
#endif // _FCITX_UI_CLASSIC_INPUTWINDOW_H_
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2022~2022 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#include "plasmathemewatchdog.h"
|
||||
#include <fcntl.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#include <cassert>
|
||||
#include <cerrno>
|
||||
#include <csignal>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include "fcitx-utils/event.h"
|
||||
#include "fcitx-utils/eventloopinterface.h"
|
||||
#include "fcitx-utils/fs.h"
|
||||
#include "fcitx-utils/standardpaths.h"
|
||||
#include "fcitx-utils/unixfd.h"
|
||||
#include "common.h"
|
||||
|
||||
#if defined(__FreeBSD__)
|
||||
#include <sys/procctl.h>
|
||||
#elif defined(__linux__)
|
||||
#include <sys/prctl.h>
|
||||
#endif
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
#define PLASMA_THEME_GENERATOR "fcitx5-plasma-theme-generator";
|
||||
|
||||
bool PlasmaThemeWatchdog::isAvailable() {
|
||||
static const std::string binaryName = PLASMA_THEME_GENERATOR;
|
||||
return StandardPaths::hasExecutable(binaryName);
|
||||
}
|
||||
|
||||
PlasmaThemeWatchdog::PlasmaThemeWatchdog(EventLoop *event,
|
||||
std::function<void()> callback)
|
||||
: callback_(std::move(callback)) {
|
||||
int pipefd[2];
|
||||
int ret = ::pipe(pipefd);
|
||||
if (ret == -1) {
|
||||
throw std::runtime_error("Failed to create pipe");
|
||||
}
|
||||
::fcntl(pipefd[0], F_SETFD, FD_CLOEXEC);
|
||||
::fcntl(pipefd[0], F_SETFL, ::fcntl(pipefd[0], F_GETFL) | O_NONBLOCK);
|
||||
::fcntl(pipefd[1], F_SETFL, ::fcntl(pipefd[1], F_GETFL) | O_NONBLOCK);
|
||||
auto pid = fork();
|
||||
if (pid == 0) {
|
||||
close(pipefd[0]);
|
||||
#if defined(__FreeBSD__)
|
||||
int procctl_value = SIGKILL;
|
||||
procctl(P_PID, 0, PROC_PDEATHSIG_CTL, &procctl_value);
|
||||
#elif defined(__linux__)
|
||||
prctl(PR_SET_PDEATHSIG, SIGKILL);
|
||||
#endif
|
||||
// Redirect stdin to /dev/null.
|
||||
UnixFD fd = UnixFD::own(open("/dev/null", O_RDWR | O_CLOEXEC));
|
||||
|
||||
if (!fd.isValid()) {
|
||||
_exit(1);
|
||||
}
|
||||
|
||||
if (fd.fd() < 3) {
|
||||
UnixFD copyFd(fd.fd(), 3);
|
||||
if (!copyFd.isValid()) {
|
||||
_exit(1);
|
||||
}
|
||||
fd = std::move(copyFd);
|
||||
}
|
||||
assert(fd.fd() >= 3);
|
||||
if (dup2(fd.fd(), STDIN_FILENO) < 0) {
|
||||
_exit(1);
|
||||
}
|
||||
fd.reset();
|
||||
|
||||
signal(SIGINT, SIG_IGN);
|
||||
char arg0[] = PLASMA_THEME_GENERATOR;
|
||||
char arg1[] = "--fd";
|
||||
std::string value = std::to_string(pipefd[1]);
|
||||
char *args[] = {arg0, arg1, value.data(), nullptr};
|
||||
execvp(arg0, args);
|
||||
_exit(1);
|
||||
} else {
|
||||
close(pipefd[1]);
|
||||
monitorFD_.give(pipefd[0]);
|
||||
generator_ = pid;
|
||||
running_ = true;
|
||||
ioEvent_ = event->addIOEvent(
|
||||
monitorFD_.fd(),
|
||||
{IOEventFlag::In, IOEventFlag::Err, IOEventFlag::Hup},
|
||||
[this, event](EventSourceIO *, int fd, IOEventFlags flags) {
|
||||
if (flags.testAny(
|
||||
IOEventFlags{IOEventFlag::Err, IOEventFlag::Hup})) {
|
||||
cleanup();
|
||||
return true;
|
||||
}
|
||||
if (!flags.test(IOEventFlag::In)) {
|
||||
return true;
|
||||
}
|
||||
ssize_t result = 0;
|
||||
do {
|
||||
uint8_t dummy;
|
||||
result = fs::safeRead(fd, &dummy, sizeof(dummy));
|
||||
} while (result > 0);
|
||||
// Check if pipe EOF, or other errors.
|
||||
if (result == 0 || (result < 0 && errno != EAGAIN)) {
|
||||
cleanup();
|
||||
return true;
|
||||
}
|
||||
timerEvent_ = event->addTimeEvent(
|
||||
CLOCK_MONOTONIC, now(CLOCK_MONOTONIC) + 1000000, 0,
|
||||
[this](EventSourceTime *, uint64_t) {
|
||||
callback_();
|
||||
return true;
|
||||
});
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
PlasmaThemeWatchdog::~PlasmaThemeWatchdog() {
|
||||
destruct_ = true;
|
||||
cleanup();
|
||||
}
|
||||
|
||||
void PlasmaThemeWatchdog::cleanup() {
|
||||
running_ = false;
|
||||
CLASSICUI_INFO() << "Cleanup Plasma Theme generator.";
|
||||
if (!destruct_) {
|
||||
// For certain distribution, it is possible that
|
||||
// fcitx5-plasma-theme-generator command presents, but not usable since
|
||||
// plasma is set to be an optional dependency to fcitx5-configtool. If
|
||||
// that happens, we need to notify to reload the theme.
|
||||
callback_();
|
||||
}
|
||||
if (generator_ == 0) {
|
||||
return;
|
||||
}
|
||||
int stat = 0;
|
||||
int ret;
|
||||
kill(generator_, SIGKILL);
|
||||
do {
|
||||
ret = waitpid(generator_, &stat, WNOHANG);
|
||||
} while (ret == -1 && errno == EINTR);
|
||||
generator_ = 0;
|
||||
ioEvent_.reset();
|
||||
}
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2022~2022 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#ifndef _FCITX5_UI_CLASSIC_PLASMATHEMEWATCHDOG_H_
|
||||
#define _FCITX5_UI_CLASSIC_PLASMATHEMEWATCHDOG_H_
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include "fcitx-utils/event.h"
|
||||
#include "fcitx-utils/eventloopinterface.h"
|
||||
#include "fcitx-utils/unixfd.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
class PlasmaThemeWatchdog {
|
||||
public:
|
||||
PlasmaThemeWatchdog(EventLoop *event, std::function<void()> callback);
|
||||
|
||||
~PlasmaThemeWatchdog();
|
||||
|
||||
bool isRunning() const { return running_; }
|
||||
|
||||
static bool isAvailable();
|
||||
|
||||
private:
|
||||
void cleanup();
|
||||
std::function<void()> callback_;
|
||||
UnixFD monitorFD_;
|
||||
std::unique_ptr<EventSourceIO> ioEvent_;
|
||||
std::unique_ptr<EventSourceTime> timerEvent_;
|
||||
pid_t generator_ = 0;
|
||||
bool destruct_ = false;
|
||||
bool running_ = false;
|
||||
};
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
|
||||
#endif // _FCITX5_UI_CLASSIC_PLASMATHEMEWATCHDOG_H_
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2023 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
|
||||
#include "portalsettingmonitor.h"
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include "fcitx-utils/dbus/bus.h"
|
||||
#include "fcitx-utils/dbus/matchrule.h"
|
||||
#include "fcitx-utils/dbus/message.h"
|
||||
#include "fcitx-utils/misc_p.h"
|
||||
#include "common.h"
|
||||
|
||||
namespace fcitx {
|
||||
namespace {
|
||||
constexpr const char XDG_PORTAL_DESKTOP_SERVICE[] =
|
||||
"org.freedesktop.portal.Desktop";
|
||||
constexpr const char XDG_PORTAL_DESKTOP_PATH[] =
|
||||
"/org/freedesktop/portal/desktop";
|
||||
constexpr const char XDG_PORTAL_DESKTOP_SETTINGS_INTERFACE[] =
|
||||
"org.freedesktop.portal.Settings";
|
||||
constexpr size_t PORTAL_RETRY_LIMIT = 3;
|
||||
} // namespace
|
||||
|
||||
PortalSettingMonitor::PortalSettingMonitor(dbus::Bus &bus)
|
||||
: bus_(bus), serviceWatcher_(bus),
|
||||
watcherMap_(
|
||||
[this](const PortalSettingKey &key) {
|
||||
PortalSettingData data{};
|
||||
data.matchSlot = bus_.addMatch(
|
||||
dbus::MatchRule(XDG_PORTAL_DESKTOP_SERVICE,
|
||||
XDG_PORTAL_DESKTOP_PATH,
|
||||
XDG_PORTAL_DESKTOP_SETTINGS_INTERFACE,
|
||||
"SettingChanged", {key.interface, key.name}),
|
||||
[this, key](dbus::Message &msg) {
|
||||
std::string interface;
|
||||
std::string name;
|
||||
msg >> interface >> name;
|
||||
if (interface != key.interface || name != key.name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
dbus::Variant variant;
|
||||
msg >> variant;
|
||||
if (variant.signature() == "v") {
|
||||
variant = variant.dataAs<dbus::Variant>();
|
||||
}
|
||||
watcherData_[key].querySlot.reset();
|
||||
|
||||
auto view = watcherMap_.view(key);
|
||||
for (auto &entry : view) {
|
||||
entry(variant);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (!data.matchSlot) {
|
||||
return false;
|
||||
}
|
||||
auto [iter, success] = watcherData_.emplace(
|
||||
std::piecewise_construct, std::forward_as_tuple(key),
|
||||
std::forward_as_tuple(std::move(data)));
|
||||
iter->second.querySlot = queryValue(key);
|
||||
return true;
|
||||
},
|
||||
[this](const PortalSettingKey &key) { watcherData_.erase(key); }) {
|
||||
|
||||
serviceWatcherEntry_ = serviceWatcher_.watchService(
|
||||
XDG_PORTAL_DESKTOP_SERVICE,
|
||||
[this](const std::string &, const std::string &,
|
||||
const std::string &newOwner) {
|
||||
setPortalServiceOwner(newOwner);
|
||||
});
|
||||
}
|
||||
|
||||
std::unique_ptr<PortalSettingEntry>
|
||||
PortalSettingMonitor::watch(const std::string &interface,
|
||||
const std::string &name,
|
||||
PortalSettingCallback callback) {
|
||||
return watcherMap_.add({interface, name}, callback);
|
||||
}
|
||||
|
||||
void PortalSettingMonitor::setPortalServiceOwner(const std::string &name) {
|
||||
if (serviceName_ == name) {
|
||||
return;
|
||||
}
|
||||
serviceName_ = name;
|
||||
if (serviceName_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
CLASSICUI_INFO() << "A new portal show up, start a new query.";
|
||||
for (auto &[key, data] : watcherData_) {
|
||||
// Reset retry since it's a new service.
|
||||
data.retry = 0;
|
||||
data.querySlot = queryValue(key);
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<dbus::Slot>
|
||||
PortalSettingMonitor::queryValue(const PortalSettingKey &key) {
|
||||
auto call = bus_.createMethodCall(
|
||||
XDG_PORTAL_DESKTOP_SERVICE, XDG_PORTAL_DESKTOP_PATH,
|
||||
XDG_PORTAL_DESKTOP_SETTINGS_INTERFACE, "Read");
|
||||
call << key.interface << key.name;
|
||||
return call.callAsync(5000000, [this, key](dbus::Message &msg) {
|
||||
// Key does not exist.
|
||||
auto *data = findValue(watcherData_, key);
|
||||
if (!data) {
|
||||
return true;
|
||||
}
|
||||
// Key itself may be gone later, put it on the stack.
|
||||
// XDG portal seems didn't unwrap the variant.
|
||||
// Check this special case just in case.
|
||||
if (msg.isError()) {
|
||||
CLASSICUI_DEBUG() << "DBus call error: " << msg.errorName() << " "
|
||||
<< msg.errorMessage();
|
||||
if (msg.errorName() == "org.freedesktop.DBus.Error.NoReply") {
|
||||
if (data->retry < PORTAL_RETRY_LIMIT) {
|
||||
data->retry += 1;
|
||||
data->querySlot = queryValue(key);
|
||||
} else {
|
||||
CLASSICUI_DEBUG()
|
||||
<< "Query portal value reaches retry limit.";
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (msg.signature() != "v") {
|
||||
return true;
|
||||
}
|
||||
dbus::Variant variant;
|
||||
msg >> variant;
|
||||
if (variant.signature() == "v") {
|
||||
variant = variant.dataAs<dbus::Variant>();
|
||||
}
|
||||
auto view = watcherMap_.view(key);
|
||||
for (auto &entry : view) {
|
||||
entry(variant);
|
||||
}
|
||||
data->querySlot.reset();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace fcitx
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2023 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#ifndef _FCITX_UI_CLASSIC_PORTALSETTINGMONITOR_H_
|
||||
#define _FCITX_UI_CLASSIC_PORTALSETTINGMONITOR_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include "fcitx-utils/dbus/bus.h"
|
||||
#include "fcitx-utils/dbus/servicewatcher.h"
|
||||
#include "fcitx-utils/dbus/variant.h"
|
||||
#include "fcitx-utils/handlertable_details.h"
|
||||
#include "fcitx-utils/macros.h"
|
||||
#include "fcitx/misc_p.h"
|
||||
|
||||
namespace fcitx {
|
||||
|
||||
struct PortalSettingKey {
|
||||
std::string interface;
|
||||
std::string name;
|
||||
|
||||
bool operator==(const PortalSettingKey &other) const {
|
||||
return interface == other.interface && name == other.name;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace fcitx
|
||||
|
||||
template <>
|
||||
struct std::hash<fcitx::PortalSettingKey> {
|
||||
std::size_t operator()(const fcitx::PortalSettingKey &key) const noexcept {
|
||||
std::size_t seed = 0;
|
||||
fcitx::hash_combine(seed, std::hash<std::string>()(key.interface));
|
||||
fcitx::hash_combine(seed, std::hash<std::string>()(key.name));
|
||||
return seed;
|
||||
}
|
||||
};
|
||||
|
||||
namespace fcitx {
|
||||
|
||||
using PortalSettingCallback = std::function<void(const dbus::Variant &)>;
|
||||
using PortalSettingEntry = HandlerTableEntry<PortalSettingCallback>;
|
||||
|
||||
class PortalSettingMonitor {
|
||||
struct PortalSettingData {
|
||||
std::unique_ptr<dbus::Slot> matchSlot;
|
||||
std::unique_ptr<dbus::Slot> querySlot;
|
||||
size_t retry = 0;
|
||||
};
|
||||
|
||||
public:
|
||||
PortalSettingMonitor(dbus::Bus &bus);
|
||||
|
||||
FCITX_NODISCARD std::unique_ptr<PortalSettingEntry>
|
||||
watch(const std::string &interface, const std::string &name,
|
||||
PortalSettingCallback callback);
|
||||
|
||||
private:
|
||||
void setPortalServiceOwner(const std::string &name);
|
||||
std::unique_ptr<dbus::Slot> queryValue(const PortalSettingKey &key);
|
||||
dbus::Bus &bus_;
|
||||
std::string serviceName_;
|
||||
dbus::ServiceWatcher serviceWatcher_;
|
||||
std::unique_ptr<HandlerTableEntry<dbus::ServiceWatcherCallback>>
|
||||
serviceWatcherEntry_;
|
||||
MultiHandlerTable<PortalSettingKey, PortalSettingCallback> watcherMap_;
|
||||
std::unordered_map<PortalSettingKey, PortalSettingData> watcherData_;
|
||||
};
|
||||
|
||||
} // namespace fcitx
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,469 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2016-2016 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#ifndef _FCITX_UI_CLASSIC_THEME_H_
|
||||
#define _FCITX_UI_CLASSIC_THEME_H_
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#include <cairo.h>
|
||||
#include "fcitx-config/configuration.h"
|
||||
#include "fcitx-config/enum.h"
|
||||
#include "fcitx-config/option.h"
|
||||
#include "fcitx-config/rawconfig.h"
|
||||
#include "fcitx-utils/color.h"
|
||||
#include "fcitx-utils/i18n.h"
|
||||
#include "fcitx-utils/i18nstring.h"
|
||||
#include "fcitx-utils/misc.h"
|
||||
#include "fcitx-utils/rect.h"
|
||||
#include "fcitx/icontheme.h"
|
||||
#include "common.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
enum class Gravity {
|
||||
TopLeft,
|
||||
TopCenter,
|
||||
TopRight,
|
||||
CenterLeft,
|
||||
Center,
|
||||
CenterRight,
|
||||
BottomLeft,
|
||||
BottomCenter,
|
||||
BottomRight
|
||||
};
|
||||
FCITX_CONFIG_ENUM_NAME_WITH_I18N(Gravity, N_("Top Left"), N_("Top Center"),
|
||||
N_("Top Right"), N_("Center Left"),
|
||||
N_("Center"), N_("Center Right"),
|
||||
N_("Bottom Left"), N_("Bottom Center"),
|
||||
N_("Bottom Right"));
|
||||
|
||||
enum class PageButtonAlignment {
|
||||
Top,
|
||||
FirstCandidate,
|
||||
Center,
|
||||
LastCandidate,
|
||||
Bottom
|
||||
};
|
||||
FCITX_CONFIG_ENUM_NAME_WITH_I18N(PageButtonAlignment, N_("Top"),
|
||||
N_("First Candidate"), N_("Center"),
|
||||
N_("Last Candidate"), N_("Bottom"));
|
||||
|
||||
enum class ColorField {
|
||||
InputPanel_Background,
|
||||
InputPanel_Border,
|
||||
InputPanel_HighlightCandidateBackground,
|
||||
InputPanel_HighlightCandidateBorder,
|
||||
InputPanel_Highlight,
|
||||
Menu_Background,
|
||||
Menu_Border,
|
||||
Menu_SelectedItemBackground,
|
||||
Menu_SelectedItemBorder,
|
||||
Menu_Separator,
|
||||
};
|
||||
FCITX_CONFIG_ENUM_NAME_WITH_I18N(
|
||||
ColorField, N_("Input Panel Background"), N_("Input Panel Border"),
|
||||
N_("Input Panel Highlight Candidate Background"),
|
||||
N_("Input Panel Highlight Candidate Border"), N_("Input Panel Highlight"),
|
||||
N_("Menu Background"), N_("Menu Border"),
|
||||
N_("Menu Selected Item Background"), N_("Menu Selected Item Border"),
|
||||
N_("Menu Separator"));
|
||||
|
||||
FCITX_CONFIGURATION(
|
||||
MarginConfig,
|
||||
Option<int, IntConstrain> marginLeft{this, "Left", _("Margin Left"), 0,
|
||||
IntConstrain(0)};
|
||||
Option<int, IntConstrain> marginRight{this, "Right", _("Margin Right"), 0,
|
||||
IntConstrain(0)};
|
||||
Option<int, IntConstrain> marginTop{this, "Top", _("Margin Top"), 0,
|
||||
IntConstrain(0)};
|
||||
Option<int, IntConstrain> marginBottom{this, "Bottom", _("Margin Bottom"),
|
||||
0, IntConstrain(0)};)
|
||||
|
||||
FCITX_CONFIGURATION(
|
||||
BackgroundImageConfig,
|
||||
Option<std::string> image{this, "Image", _("Background Image")};
|
||||
OptionWithAnnotation<Color, ToolTipAnnotation> color{
|
||||
this,
|
||||
"Color",
|
||||
_("Color"),
|
||||
Color("#ffffff"),
|
||||
{},
|
||||
{},
|
||||
{_("This option is only effective if image is not set.")}};
|
||||
OptionWithAnnotation<Color, ToolTipAnnotation> borderColor{
|
||||
this,
|
||||
"BorderColor",
|
||||
_("Border Color"),
|
||||
Color("#ffffff00"),
|
||||
{},
|
||||
{},
|
||||
{_("This option is only effective if image is not set.")}};
|
||||
Option<int, IntConstrain, DefaultMarshaller<int>, ToolTipAnnotation>
|
||||
borderWidth{this,
|
||||
"BorderWidth",
|
||||
_("Border width"),
|
||||
0,
|
||||
IntConstrain(0),
|
||||
{},
|
||||
{_("This value should be less than any of margin value.")}};
|
||||
Option<std::string> overlay{this, "Overlay", _("Overlay Image")};
|
||||
OptionWithAnnotation<Gravity, GravityI18NAnnotation> gravity{
|
||||
this, "Gravity", _("Overlay position")};
|
||||
Option<int> overlayOffsetX{this, "OverlayOffsetX", _("Overlay X offset")};
|
||||
Option<int> overlayOffsetY{this, "OverlayOffsetY", _("Overlay Y offset")};
|
||||
Option<bool> hideOverlayIfOversize{this, "HideOverlayIfOversize",
|
||||
_("Hide overlay if size does not fit"),
|
||||
false};
|
||||
Option<MarginConfig> margin{this, "Margin", _("Margin")};
|
||||
Option<MarginConfig> overlayClipMargin{this, "OverlayClipMargin",
|
||||
_("Overlay Clip Margin")};)
|
||||
|
||||
FCITX_CONFIGURATION_EXTEND(HighlightBackgroundImageConfig,
|
||||
BackgroundImageConfig,
|
||||
Option<MarginConfig> clickMargin{
|
||||
this, "HighlightClickMargin",
|
||||
_("Highlight Click Margin")};);
|
||||
|
||||
FCITX_CONFIGURATION(ActionImageConfig,
|
||||
Option<std::string> image{this, "Image", _("Image")};
|
||||
Option<MarginConfig> clickMargin{this, "ClickMargin",
|
||||
_("Click Margin")};)
|
||||
|
||||
FCITX_CONFIGURATION(
|
||||
InputPanelThemeConfig,
|
||||
Option<Color> normalColor{this, "NormalColor", _("Normal text color"),
|
||||
Color("#000000ff")};
|
||||
Option<Color> highlightColor{this, "HighlightColor",
|
||||
_("Highlight text color"), Color("#ffffffff")};
|
||||
Option<Color> highlightBackgroundColor{this, "HighlightBackgroundColor",
|
||||
_("Highlight Background color"),
|
||||
Color("#a5a5a5ff")};
|
||||
Option<Color> highlightCandidateColor{this, "HighlightCandidateColor",
|
||||
_("Highlight Candidate Text Color"),
|
||||
Color("#ffffffff")};
|
||||
Option<int, IntConstrain> labelTextSizeFactor{
|
||||
this, "LabelTextSizeFactor", _("Candidate Label Text Size Factor"), 100,
|
||||
IntConstrain(0, 400)};
|
||||
Option<std::optional<Color>> candidateLabelColor{
|
||||
this, "CandidateLabelColor", _("Candidate Label Text Color")};
|
||||
Option<std::optional<Color>> highlightCandidateLabelColor{
|
||||
this, "HighlightCandidateLabelColor",
|
||||
_("Highlight Candidate Label Text Color")};
|
||||
Option<int, IntConstrain> commentTextSizeFactor{
|
||||
this, "CommentTextSizeFactor", _("Candidate Comment Text Size Factor"),
|
||||
75, IntConstrain(0, 400)};
|
||||
Option<std::optional<Color>> candidateCommentColor{
|
||||
this, "CandidateCommentColor", _("Candidate Comment Text Color")};
|
||||
Option<std::optional<Color>> highlightCandidateCommentColor{
|
||||
this, "HighlightCandidateCommentColor",
|
||||
_("Highlight Candidate Comment Text Color")};
|
||||
// Material 3 style. When SurfaceColor is fully transparent the panel falls
|
||||
// back to the classic image based background / highlight.
|
||||
Option<Color> surfaceColor{this, "SurfaceColor",
|
||||
_("Material panel surface color"),
|
||||
Color("#00000000")};
|
||||
Option<Color> outlineColor{this, "OutlineColor",
|
||||
_("Material panel outline color"),
|
||||
Color("#00000000")};
|
||||
Option<Color> highlightFillColor{this, "HighlightFillColor",
|
||||
_("Material highlight fill color"),
|
||||
Color("#00000000")};
|
||||
Option<Color> hoverFillColor{this, "HoverFillColor",
|
||||
_("Material hover state layer color"),
|
||||
Color("#00000000")};
|
||||
Option<int, IntConstrain> panelRadius{this, "PanelRadius",
|
||||
_("Panel corner radius"), 0,
|
||||
IntConstrain(0, 64)};
|
||||
Option<int, IntConstrain> itemRadius{this, "ItemRadius",
|
||||
_("Candidate corner radius"), 0,
|
||||
IntConstrain(0, 64)};
|
||||
Option<bool> enableBlur{this, "EnableBlur", _("Enable Blur on KWin"),
|
||||
false};
|
||||
Option<std::string> blurMask{this, "BlurMask", _("Blur mask"), ""};
|
||||
Option<MarginConfig> blurMargin{this, "BlurMargin", _("Blur Margin")};
|
||||
Option<bool> fullWidthHighlight{
|
||||
this, "FullWidthHighlight",
|
||||
_("Use all horizontal space for highlight when it is vertical list"),
|
||||
true};
|
||||
OptionWithAnnotation<PageButtonAlignment, PageButtonAlignmentI18NAnnotation>
|
||||
buttonAlignment{this, "PageButtonAlignment",
|
||||
_("Page button vertical alignment"),
|
||||
PageButtonAlignment::Bottom};
|
||||
Option<BackgroundImageConfig> background{this, "Background",
|
||||
_("Background")};
|
||||
Option<HighlightBackgroundImageConfig> highlight{this, "Highlight",
|
||||
_("Highlight Background")};
|
||||
Option<MarginConfig> contentMargin{this, "ContentMargin",
|
||||
_("Margin around all content")};
|
||||
Option<MarginConfig> textMargin{this, "TextMargin",
|
||||
_("Margin around text")};
|
||||
Option<ActionImageConfig> prev{this, "PrevPage", _("Prev Page Button")};
|
||||
Option<ActionImageConfig> next{this, "NextPage", _("Next Page Button")};
|
||||
Option<MarginConfig> shadowMargin{this, "ShadowMargin",
|
||||
_("Shadow Margin")};);
|
||||
FCITX_CONFIGURATION(
|
||||
MenuThemeConfig,
|
||||
Option<Color> normalColor{this, "NormalColor", _("Normal text color"),
|
||||
Color("#000000ff")};
|
||||
Option<Color> highlightTextColor{this, "HighlightCandidateColor",
|
||||
_("Selected Item text color"),
|
||||
Color("#ffffffff")};
|
||||
Option<int> spacing{this, "Spacing", _("Spacing"), 0};
|
||||
Option<bool> enableBlur{this, "EnableBlur", _("Enable Blur on KWin"),
|
||||
false};
|
||||
Option<std::string> blurMask{this, "BlurMask", _("Blur mask"), ""};
|
||||
Option<MarginConfig> blurMargin{this, "BlurMargin", _("Blur Margin")};
|
||||
Option<BackgroundImageConfig> background{this, "Background",
|
||||
_("Background")};
|
||||
Option<BackgroundImageConfig> highlight{this, "Highlight",
|
||||
_("Highlight Background")};
|
||||
Option<BackgroundImageConfig> separator{this, "Separator",
|
||||
_("Separator Background")};
|
||||
Option<BackgroundImageConfig> checkBox{this, "CheckBox", _("Check box")};
|
||||
Option<BackgroundImageConfig> subMenu{this, "SubMenu", _("Sub Menu")};
|
||||
Option<MarginConfig> contentMargin{this, "ContentMargin",
|
||||
_("Margin around all content")};
|
||||
Option<MarginConfig> textMargin{this, "TextMargin",
|
||||
_("Margin around text")};);
|
||||
|
||||
FCITX_CONFIGURATION(ThemeMetadata,
|
||||
Option<I18NString> name{this, "Name", _("Name")};
|
||||
Option<int> version{this, "Version", _("Version"), 1};
|
||||
Option<std::string> author{this, "Author", _("Author")};
|
||||
Option<I18NString> description{this, "Description",
|
||||
_("Description")};)
|
||||
|
||||
FCITX_CONFIGURATION(
|
||||
ThemeConfig,
|
||||
HiddenOption<ThemeMetadata> metadata{this, "Metadata", _("Metadata")};
|
||||
Option<int, IntConstrain> supportedScale{this, "SupportedScale",
|
||||
_("Supported image scale"), 1,
|
||||
IntConstrain(1, 10)};
|
||||
Option<InputPanelThemeConfig> inputPanel{this, "InputPanel",
|
||||
_("Input Panel")};
|
||||
Option<MenuThemeConfig> menu{this, "Menu", _("Menu")};
|
||||
Option<std::vector<ColorField>> accentColor{this, "AccentColorField",
|
||||
_("Accent Colors")};);
|
||||
|
||||
class M3UI;
|
||||
class ClassicUIConfig;
|
||||
class Theme;
|
||||
|
||||
class ThemeImage {
|
||||
public:
|
||||
using Pixmap = UniqueCPtr<cairo_surface_t, cairo_surface_destroy>;
|
||||
using CairoPattern = UniqueCPtr<cairo_pattern_t, cairo_pattern_destroy>;
|
||||
enum class TilePosition {
|
||||
TopLeft,
|
||||
Top,
|
||||
TopRight,
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
BottomLeft,
|
||||
Bottom,
|
||||
BottomRight,
|
||||
};
|
||||
struct Svg {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
CairoPattern pattern;
|
||||
};
|
||||
struct Pattern {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int borderWidth = 0;
|
||||
CairoPattern pattern;
|
||||
};
|
||||
struct NineTiles {
|
||||
struct Tile {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int sourceX = 0;
|
||||
int sourceY = 0;
|
||||
CairoPattern pattern;
|
||||
};
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
std::array<Tile, 9> tiles;
|
||||
};
|
||||
|
||||
ThemeImage(const Theme &theme, const BackgroundImageConfig &cfg,
|
||||
const Color &color, const Color &borderColor);
|
||||
ThemeImage(const Theme &theme, const ActionImageConfig &cfg);
|
||||
ThemeImage(const IconTheme &iconTheme, const std::string &icon,
|
||||
const std::string &label, uint32_t size,
|
||||
const M3UI *classicui);
|
||||
|
||||
static void drawTextIcon(cairo_surface_t *surface, const std::string &label,
|
||||
uint32_t size, const ClassicUIConfig &config);
|
||||
|
||||
auto height() const { return image_ ? image_->height : 0; }
|
||||
auto width() const { return image_ ? image_->width : 0; }
|
||||
|
||||
auto size() const { return size_; }
|
||||
|
||||
bool valid() const { return image_.has_value(); }
|
||||
bool hasOverlay() const { return overlay_.has_value(); }
|
||||
auto overlayWidth() const { return overlay_ ? overlay_->width : 0; }
|
||||
auto overlayHeight() const { return overlay_ ? overlay_->height : 0; }
|
||||
bool isPattern() const { return isPattern_; }
|
||||
void paintRegion(cairo_t *c, TilePosition tile, double destX, double destY,
|
||||
double destWidth, double destHeight, double alpha = 1.0,
|
||||
bool overlay = false) const;
|
||||
|
||||
private:
|
||||
std::string currentText_;
|
||||
uint32_t size_ = 0;
|
||||
bool isImage_ = false;
|
||||
|
||||
std::optional<NineTiles> image_;
|
||||
std::optional<NineTiles::Tile> overlay_;
|
||||
std::map<int, NineTiles> scaledImages_;
|
||||
std::map<int, NineTiles::Tile> scaledOverlays_;
|
||||
bool isPattern_ = false;
|
||||
};
|
||||
|
||||
class Theme : public ThemeConfig {
|
||||
public:
|
||||
Theme();
|
||||
~Theme();
|
||||
|
||||
const std::string &name() const { return name_; }
|
||||
void load(std::string_view name);
|
||||
void load(std::string_view name, const RawConfig &rawConfig);
|
||||
const ThemeImage &loadImage(const std::string &icon,
|
||||
const std::string &label, uint32_t size,
|
||||
const M3UI *classicui);
|
||||
const ThemeImage &loadBackground(const BackgroundImageConfig &cfg);
|
||||
const ThemeImage &loadAction(const ActionImageConfig &cfg);
|
||||
|
||||
void paint(cairo_t *c, const BackgroundImageConfig &cfg, double dx,
|
||||
double dy, double width, double height, double alpha);
|
||||
|
||||
void paint(cairo_t *c, const ActionImageConfig &cfg, double alpha = 1.0);
|
||||
|
||||
std::vector<Rect> mask(const BackgroundImageConfig &cfg, int width,
|
||||
int height);
|
||||
|
||||
bool setIconTheme(const std::string &name);
|
||||
|
||||
const auto &maskConfig() const { return maskConfig_; }
|
||||
const auto &accentColorFields() const { return accentColorFields_; }
|
||||
|
||||
void populateColor(std::optional<Color> accent);
|
||||
|
||||
const auto &inputPanelBackground() const { return inputPanelBackground_; }
|
||||
const auto &inputPanelBorder() const { return inputPanelBorder_; }
|
||||
const auto &inputPanelHighlightCandidateBackground() const {
|
||||
return inputPanelHighlightCandidateBackground_;
|
||||
}
|
||||
const auto &inputPanelHighlightCandidateBorder() const {
|
||||
return inputPanelHighlightCandidateBorder_;
|
||||
}
|
||||
const auto &inputPanelHighlight() const { return inputPanelHighlight_; }
|
||||
const auto &inputPanelText() const { return inputPanelText_; }
|
||||
const auto &inputPanelHighlightText() const {
|
||||
return inputPanelHighlightText_;
|
||||
}
|
||||
const auto &inputPanelHighlightCandidateText() const {
|
||||
return inputPanelHighlightCandidateText_;
|
||||
}
|
||||
const auto &inputPanelCandidateLabelText() const {
|
||||
return inputPanelCandidateLabelText_;
|
||||
}
|
||||
const auto &inputPanelHighlightCandidateLabelText() const {
|
||||
return inputPanelHighlightCandidateLabelText_;
|
||||
}
|
||||
const auto &inputPanelCandidateCommentText() const {
|
||||
return inputPanelCandidateCommentText_;
|
||||
}
|
||||
const auto &inputPanelHighlightCandidateCommentText() const {
|
||||
return inputPanelHighlightCandidateCommentText_;
|
||||
}
|
||||
|
||||
const auto &menuBackground() const { return menuBackground_; }
|
||||
const auto &menuBorder() const { return menuBorder_; }
|
||||
const auto &menuSelectedItemBackground() const {
|
||||
return menuSelectedItemBackground_;
|
||||
}
|
||||
const auto &menuSelectedItemBorder() const {
|
||||
return menuSelectedItemBorder_;
|
||||
}
|
||||
const auto &menuSeparator() const { return menuSeparator_; }
|
||||
const auto &menuText() const { return menuText_; }
|
||||
const auto &menuSelectedItemText() const { return menuSelectedItemText_; }
|
||||
const auto &menuBlurMaskConfig() const { return menuBlurMaskConfig_; }
|
||||
|
||||
bool isSystemTheme() const { return isSystemTheme_; }
|
||||
|
||||
static bool isSystemThemeName(std::string_view themeName);
|
||||
|
||||
private:
|
||||
void reset();
|
||||
|
||||
std::unordered_map<const BackgroundImageConfig *, ThemeImage>
|
||||
backgroundImageTable_;
|
||||
std::unordered_map<const ActionImageConfig *, ThemeImage> actionImageTable_;
|
||||
std::unordered_map<std::string, ThemeImage> trayImageTable_;
|
||||
IconTheme iconTheme_;
|
||||
std::string name_;
|
||||
BackgroundImageConfig maskConfig_;
|
||||
BackgroundImageConfig menuBlurMaskConfig_;
|
||||
std::unordered_set<ColorField> accentColorFields_;
|
||||
|
||||
Color inputPanelBackground_;
|
||||
Color inputPanelBorder_;
|
||||
Color inputPanelHighlightCandidateBackground_;
|
||||
Color inputPanelHighlightCandidateBorder_;
|
||||
Color inputPanelHighlight_;
|
||||
Color inputPanelText_;
|
||||
Color inputPanelHighlightText_;
|
||||
Color inputPanelHighlightCandidateText_;
|
||||
|
||||
Color inputPanelCandidateLabelText_;
|
||||
Color inputPanelHighlightCandidateLabelText_;
|
||||
Color inputPanelCandidateCommentText_;
|
||||
Color inputPanelHighlightCandidateCommentText_;
|
||||
|
||||
Color menuBackground_;
|
||||
Color menuBorder_;
|
||||
Color menuSelectedItemBackground_;
|
||||
Color menuSelectedItemBorder_;
|
||||
Color menuSeparator_;
|
||||
Color menuText_;
|
||||
Color menuSelectedItemText_;
|
||||
|
||||
bool isSystemTheme_ = false;
|
||||
};
|
||||
|
||||
inline void cairoSetSourceColor(cairo_t *cr, const Color &color) {
|
||||
cairo_set_source_rgba(cr, color.redF(), color.greenF(), color.blueF(),
|
||||
color.alphaF());
|
||||
}
|
||||
|
||||
inline void shrink(Rect &rect, const MarginConfig &margin) {
|
||||
int newWidth = rect.width() - *margin.marginLeft - *margin.marginRight;
|
||||
int newHeight = rect.height() - *margin.marginTop - *margin.marginBottom;
|
||||
newWidth = std::max(newWidth, 0);
|
||||
newHeight = std::max(newHeight, 0);
|
||||
rect.setPosition(rect.left() + *margin.marginLeft,
|
||||
rect.top() + *margin.marginTop);
|
||||
rect.setSize(newWidth, newHeight);
|
||||
}
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
|
||||
#endif // _FCITX_UI_CLASSIC_THEME_H_
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2023 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#include "waylandcursor.h"
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <wayland-client-protocol.h>
|
||||
#include <wayland-cursor.h>
|
||||
#include "fcitx-utils/event.h"
|
||||
#include "fcitx-utils/eventloopinterface.h"
|
||||
#include "display.h"
|
||||
#include "waylandpointer.h"
|
||||
#include "waylandui.h"
|
||||
#include "wl_callback.h"
|
||||
#include "wl_compositor.h"
|
||||
#include "wl_output.h"
|
||||
#include "wl_surface.h"
|
||||
#include "wp_cursor_shape_manager_v1.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
WaylandCursor::WaylandCursor(WaylandPointer *pointer)
|
||||
: pointer_(pointer), animationStart_(now(CLOCK_MONOTONIC)) {
|
||||
auto *display = pointer->ui()->display();
|
||||
time_ = pointer->ui()->parent()->instance()->eventLoop().addTimeEvent(
|
||||
CLOCK_MONOTONIC, 0, 1000, [this](EventSourceTime *, uint64_t) {
|
||||
timerCallback();
|
||||
return true;
|
||||
});
|
||||
time_->setEnabled(false);
|
||||
display->requestGlobals<wayland::WpCursorShapeManagerV1>();
|
||||
cursorShapeGlobalConn_ = display->globalCreated().connect(
|
||||
[this](const std::string &name, const std::shared_ptr<void> &) {
|
||||
if (name == wayland::WpCursorShapeManagerV1::interface) {
|
||||
setupCursorShape();
|
||||
}
|
||||
});
|
||||
setupCursorShape();
|
||||
}
|
||||
|
||||
void WaylandCursor::setupCursorShape() {
|
||||
auto cursorShape =
|
||||
pointer_->ui()->display()->getGlobal<wayland::WpCursorShapeManagerV1>();
|
||||
if (!cursorShape) {
|
||||
return;
|
||||
}
|
||||
cursorShape_.reset(cursorShape->getPointer(pointer_->pointer()));
|
||||
}
|
||||
|
||||
wayland::WlSurface *WaylandCursor::getOrCreateSurface() {
|
||||
if (!surface_) {
|
||||
auto compositor =
|
||||
pointer_->ui()->display()->getGlobal<wayland::WlCompositor>();
|
||||
surface_.reset(compositor->createSurface());
|
||||
surface_->enter().connect([this](wayland::WlOutput *output) {
|
||||
const auto *info =
|
||||
pointer_->ui()->display()->outputInformation(output);
|
||||
if (!info) {
|
||||
return;
|
||||
}
|
||||
if (outputScale_ != info->scale()) {
|
||||
outputScale_ = info->scale();
|
||||
update();
|
||||
}
|
||||
});
|
||||
}
|
||||
return surface_.get();
|
||||
}
|
||||
|
||||
int32_t WaylandCursor::scale() {
|
||||
auto outputs = pointer_->ui()->display()->getGlobals<wayland::WlOutput>();
|
||||
int32_t scale = 1;
|
||||
if (outputScale_) {
|
||||
return *outputScale_;
|
||||
}
|
||||
for (const auto &output : outputs) {
|
||||
const auto *info =
|
||||
pointer_->ui()->display()->outputInformation(output.get());
|
||||
if (!info) {
|
||||
continue;
|
||||
}
|
||||
scale = std::max(scale, info->scale());
|
||||
}
|
||||
return scale;
|
||||
}
|
||||
|
||||
void WaylandCursor::update() {
|
||||
if (!pointer_->enterSerial()) {
|
||||
return;
|
||||
}
|
||||
if (cursorShape_) {
|
||||
cursorShape_->setShape(pointer_->enterSerial(),
|
||||
WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT);
|
||||
surface_.reset();
|
||||
return;
|
||||
}
|
||||
auto info = pointer_->ui()->cursorTheme()->loadCursorTheme(scale());
|
||||
auto *surface = getOrCreateSurface();
|
||||
if (info.theme != theme_) {
|
||||
surface->attach(nullptr, 0, 0);
|
||||
surface->commit();
|
||||
theme_ = info.theme;
|
||||
}
|
||||
auto *cursor = info.cursor;
|
||||
if (!cursor) {
|
||||
return;
|
||||
}
|
||||
uint32_t duration = 0;
|
||||
int frame = wl_cursor_frame_and_duration(
|
||||
cursor, (now(CLOCK_MONOTONIC) - animationStart_) / 1000, &duration);
|
||||
|
||||
auto bufferScale = scale();
|
||||
|
||||
int pixelSize = std::max(
|
||||
cursor->images[frame]->width,
|
||||
cursor->images[frame]->height); // Not all cursor themes are square
|
||||
while (bufferScale > 1 && pixelSize / bufferScale <
|
||||
pointer_->ui()->cursorTheme()->cursorSize()) {
|
||||
--bufferScale;
|
||||
}
|
||||
|
||||
// Do not attach width/height that is not divisible by scale.
|
||||
if (cursor->images[frame]->width % bufferScale != 0 ||
|
||||
cursor->images[frame]->height % bufferScale != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
pointer_->pointer()->setCursor(
|
||||
pointer_->enterSerial(), surface,
|
||||
cursor->images[frame]->hotspot_x / bufferScale,
|
||||
cursor->images[frame]->hotspot_y / bufferScale);
|
||||
surface->setBufferScale(bufferScale);
|
||||
|
||||
frameCalled_ = false;
|
||||
timerCalled_ = false;
|
||||
wl_surface_attach(*surface,
|
||||
wl_cursor_image_get_buffer(cursor->images[frame]), 0, 0);
|
||||
surface->damage(0, 0, cursor->images[frame]->width,
|
||||
cursor->images[frame]->height);
|
||||
|
||||
std::unique_ptr<wayland::WlCallback> newCallback;
|
||||
if (duration) {
|
||||
newCallback.reset(surface->frame());
|
||||
time_->setOneShot();
|
||||
time_->setNextInterval(duration * 1000ULL);
|
||||
newCallback->done().connect([this](uint32_t) { frameCallback(); });
|
||||
}
|
||||
|
||||
surface->commit();
|
||||
// Ensure callback_ is the last variable being destroyed.
|
||||
callback_ = std::move(newCallback);
|
||||
}
|
||||
|
||||
void WaylandCursor::timerCallback() {
|
||||
timerCalled_ = true;
|
||||
maybeUpdate();
|
||||
}
|
||||
|
||||
void WaylandCursor::frameCallback() {
|
||||
frameCalled_ = true;
|
||||
maybeUpdate();
|
||||
}
|
||||
|
||||
void WaylandCursor::maybeUpdate() {
|
||||
if (frameCalled_ && timerCalled_) {
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2023 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#ifndef _FCITX_UI_CLASSIC_WAYLANDCURSOR_H_
|
||||
#define _FCITX_UI_CLASSIC_WAYLANDCURSOR_H_
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <wayland-cursor.h>
|
||||
#include "fcitx-utils/event.h"
|
||||
#include "fcitx-utils/signals.h"
|
||||
#include "display.h"
|
||||
#include "wl_callback.h"
|
||||
#include "wl_surface.h"
|
||||
#include "wp_cursor_shape_device_v1.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
class WaylandPointer;
|
||||
|
||||
class WaylandCursor {
|
||||
public:
|
||||
WaylandCursor(WaylandPointer *pointer);
|
||||
|
||||
// Size and hotspot are in surface coordinates
|
||||
void update();
|
||||
|
||||
private:
|
||||
int32_t scale();
|
||||
void timerCallback();
|
||||
void frameCallback();
|
||||
void maybeUpdate();
|
||||
void setupCursorShape();
|
||||
wayland::WlSurface *getOrCreateSurface();
|
||||
WaylandPointer *pointer_;
|
||||
uint64_t animationStart_;
|
||||
|
||||
// We keep a reference to the
|
||||
std::shared_ptr<wl_cursor_theme> theme_;
|
||||
std::unique_ptr<wayland::WlSurface> surface_;
|
||||
std::unique_ptr<wayland::WlCallback> callback_;
|
||||
std::unique_ptr<EventSourceTime> time_;
|
||||
bool timerCalled_ = false;
|
||||
bool frameCalled_ = false;
|
||||
std::optional<int32_t> outputScale_;
|
||||
ScopedConnection cursorShapeGlobalConn_;
|
||||
std::unique_ptr<wayland::WpCursorShapeDeviceV1> cursorShape_;
|
||||
};
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,105 @@
|
||||
#include "waylandcursortheme.h"
|
||||
#include <charconv>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
#include <wayland-cursor.h>
|
||||
#include "fcitx-utils/environ.h"
|
||||
#include "fcitx-utils/misc_p.h"
|
||||
#include "waylandui.h"
|
||||
#include "wl_shm.h"
|
||||
|
||||
#ifdef ENABLE_DBUS
|
||||
#include "fcitx-utils/dbus/variant.h"
|
||||
#include "dbus_public.h"
|
||||
#include "portalsettingmonitor.h"
|
||||
#endif
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
WaylandCursorTheme::WaylandCursorTheme(WaylandUI *ui)
|
||||
: shm_(ui->display()->getGlobal<wayland::WlShm>()) {
|
||||
|
||||
if (auto size = getEnvironment("XCURSOR_SIZE")) {
|
||||
unsigned int cursorSize = 0;
|
||||
if (std::from_chars(size->data(), size->data() + size->size(),
|
||||
cursorSize)
|
||||
.ec == std::errc()) {
|
||||
setCursorSize(cursorSize);
|
||||
}
|
||||
}
|
||||
|
||||
if (auto theme = getEnvironment("XCURSOR_THEME")) {
|
||||
setTheme(*theme);
|
||||
} else {
|
||||
setTheme({});
|
||||
}
|
||||
|
||||
#ifdef ENABLE_DBUS
|
||||
if (auto *dbusAddon = ui->parent()->dbus()) {
|
||||
settingMonitor_ = std::make_unique<PortalSettingMonitor>(
|
||||
*dbusAddon->call<IDBusModule::bus>());
|
||||
cursorSizeWatcher_ =
|
||||
settingMonitor_->watch("org.gnome.desktop.interface", "cursor-size",
|
||||
[this](const dbus::Variant &value) {
|
||||
if (value.signature() == "i") {
|
||||
setCursorSize(value.dataAs<int>());
|
||||
}
|
||||
});
|
||||
cursorThemeWatcher_ = settingMonitor_->watch(
|
||||
"org.gnome.desktop.interface", "cursor-theme",
|
||||
[this](const dbus::Variant &value) {
|
||||
if (value.signature() == "s") {
|
||||
setTheme(value.dataAs<std::string>());
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void WaylandCursorTheme::setCursorSize(int cursorSize) {
|
||||
// Add some simple validation.
|
||||
int newCursorSize = 24;
|
||||
if (cursorSize > 0 && cursorSize < 2048) {
|
||||
newCursorSize = cursorSize;
|
||||
}
|
||||
|
||||
if (newCursorSize == cursorSize_) {
|
||||
return;
|
||||
}
|
||||
cursorSize_ = newCursorSize;
|
||||
themes_.clear();
|
||||
themeChangedSignal_();
|
||||
}
|
||||
|
||||
void WaylandCursorTheme::setTheme(const std::string &theme) {
|
||||
themes_.clear();
|
||||
themeName_ = theme;
|
||||
themeChangedSignal_();
|
||||
}
|
||||
|
||||
WaylandCursorInfo WaylandCursorTheme::loadCursorTheme(int scale) {
|
||||
auto size = cursorSize_ * scale;
|
||||
if (auto *theme = findValue(themes_, size)) {
|
||||
return *theme;
|
||||
}
|
||||
WaylandCursorInfo info;
|
||||
info.theme = std::shared_ptr<wl_cursor_theme>(
|
||||
wl_cursor_theme_load(themeName_.empty() ? nullptr : themeName_.data(),
|
||||
size, *shm_),
|
||||
wl_cursor_theme_destroy);
|
||||
if (info.theme) {
|
||||
info.cursor = wl_cursor_theme_get_cursor(info.theme.get(), "default");
|
||||
if (!info.cursor) {
|
||||
info.cursor =
|
||||
wl_cursor_theme_get_cursor(info.theme.get(), "left_ptr");
|
||||
}
|
||||
}
|
||||
|
||||
auto &themePtr = themes_[size] = std::move(info);
|
||||
return themePtr;
|
||||
}
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2023 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#ifndef _FCITX_UI_CLASSIC_WAYLANDCURSORTHEME_H_
|
||||
#define _FCITX_UI_CLASSIC_WAYLANDCURSORTHEME_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <wayland-cursor.h>
|
||||
#include "fcitx-utils/signals.h"
|
||||
#include "config.h"
|
||||
#include "portalsettingmonitor.h"
|
||||
#include "wl_shm.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
class WaylandUI;
|
||||
|
||||
struct WaylandCursorInfo {
|
||||
std::shared_ptr<wl_cursor_theme> theme;
|
||||
wl_cursor *cursor = nullptr;
|
||||
};
|
||||
|
||||
class WaylandCursorTheme {
|
||||
public:
|
||||
WaylandCursorTheme(WaylandUI *ui);
|
||||
void setTheme(const std::string &theme);
|
||||
|
||||
auto &themeChanged() const { return themeChangedSignal_; }
|
||||
|
||||
WaylandCursorInfo loadCursorTheme(int scale);
|
||||
auto cursorSize() const { return cursorSize_; }
|
||||
|
||||
private:
|
||||
void setCursorSize(int cursorSize);
|
||||
void timerCallback();
|
||||
void frameCallback();
|
||||
|
||||
fcitx::Signal<void()> themeChangedSignal_;
|
||||
|
||||
std::shared_ptr<wayland::WlShm> shm_;
|
||||
// Size to theme map;
|
||||
std::unordered_map<int, WaylandCursorInfo> themes_;
|
||||
int cursorSize_ = 24;
|
||||
std::string themeName_;
|
||||
#ifdef ENABLE_DBUS
|
||||
std::unique_ptr<PortalSettingMonitor> settingMonitor_;
|
||||
std::unique_ptr<PortalSettingEntry> cursorSizeWatcher_, cursorThemeWatcher_;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2017-2017 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#include "waylandinputwindow.h"
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <cairo.h>
|
||||
#include "fcitx-utils/rect.h"
|
||||
#include "fcitx/inputcontext.h"
|
||||
#include "common.h"
|
||||
#include "ext_background_effect_manager_v1.h"
|
||||
#include "inputwindow.h"
|
||||
#include "theme.h"
|
||||
#include "waylandim_public.h"
|
||||
#include "waylandui.h"
|
||||
#include "waylandwindow.h"
|
||||
#include "wl_compositor.h"
|
||||
#include "wl_region.h"
|
||||
#include "zwp_input_method_v2.h"
|
||||
#include "zwp_input_panel_v1.h"
|
||||
|
||||
#ifdef __linux__
|
||||
#include <linux/input-event-codes.h>
|
||||
#elif __FreeBSD__
|
||||
#include <dev/evdev/input-event-codes.h>
|
||||
#else
|
||||
#define BTN_LEFT 0x110
|
||||
#define BTN_RIGHT 0x111
|
||||
#endif
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
WaylandInputWindow::WaylandInputWindow(WaylandUI *ui)
|
||||
: InputWindow(ui->parent()), ui_(ui), window_(ui->newWindow()) {
|
||||
window_->createWindow();
|
||||
window_->repaint().connect([this]() {
|
||||
if (auto *ic = repaintIC_.get()) {
|
||||
if (ic->hasFocus()) {
|
||||
update(ic);
|
||||
}
|
||||
}
|
||||
});
|
||||
window_->click().connect([this](int x, int y, uint32_t button,
|
||||
uint32_t state) {
|
||||
if (state != WL_POINTER_BUTTON_STATE_PRESSED) {
|
||||
return;
|
||||
}
|
||||
if (button == BTN_LEFT) {
|
||||
click(x, y);
|
||||
} else if (button == BTN_RIGHT) {
|
||||
rightClick(x, y);
|
||||
}
|
||||
});
|
||||
window_->hover().connect([this](int x, int y) {
|
||||
if (hover(x, y)) {
|
||||
repaint();
|
||||
}
|
||||
});
|
||||
window_->leave().connect([this]() {
|
||||
if (hover(-1, -1)) {
|
||||
repaint();
|
||||
}
|
||||
});
|
||||
window_->touchDown().connect([this](int x, int y) { click(x, y); });
|
||||
window_->touchUp().connect([](int, int) {
|
||||
// do nothing
|
||||
});
|
||||
window_->axis().connect([this](int, int, uint32_t axis, wl_fixed_t value) {
|
||||
if (axis != WL_POINTER_AXIS_VERTICAL_SCROLL) {
|
||||
return;
|
||||
}
|
||||
scroll_ += value;
|
||||
bool triggered = false;
|
||||
while (scroll_ >= 2560) {
|
||||
scroll_ -= 2560;
|
||||
wheel(/*up=*/false);
|
||||
triggered = true;
|
||||
}
|
||||
while (scroll_ <= -2560) {
|
||||
scroll_ += 2560;
|
||||
wheel(/*up=*/true);
|
||||
triggered = true;
|
||||
}
|
||||
if (triggered) {
|
||||
repaint();
|
||||
}
|
||||
});
|
||||
initPanel();
|
||||
}
|
||||
|
||||
void WaylandInputWindow::initPanel() {
|
||||
if (!window_->surface()) {
|
||||
window_->createWindow();
|
||||
updateBlur();
|
||||
}
|
||||
|
||||
setFontDPI(*parent_->config().forceWaylandDPI);
|
||||
}
|
||||
|
||||
void WaylandInputWindow::setBlurManager(
|
||||
std::shared_ptr<wayland::ExtBackgroundEffectManagerV1> blur) {
|
||||
blurManager_ = std::move(blur);
|
||||
updateBlur();
|
||||
}
|
||||
|
||||
void WaylandInputWindow::updateBlur() {
|
||||
if (!window_->surface()) {
|
||||
return;
|
||||
}
|
||||
blur_.reset();
|
||||
if (!blurManager_) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto compositor = ui_->display()->getGlobal<wayland::WlCompositor>();
|
||||
if (!compositor) {
|
||||
return;
|
||||
}
|
||||
auto width = window_->width();
|
||||
auto height = window_->height();
|
||||
Rect rect(0, 0, width, height);
|
||||
shrink(rect, *ui_->parent()->theme().inputPanel->blurMargin);
|
||||
if (!*ui_->parent()->theme().inputPanel->enableBlur || rect.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
std::vector<uint32_t> data;
|
||||
std::unique_ptr<wayland::WlRegion> region(compositor->createRegion());
|
||||
if (ui_->parent()->theme().inputPanel->blurMask->empty()) {
|
||||
region->add(rect.left(), rect.top(), rect.width(), rect.height());
|
||||
} else {
|
||||
auto regions =
|
||||
parent_->theme().mask(parent_->theme().maskConfig(), width, height);
|
||||
for (const auto &rect : regions) {
|
||||
region->add(rect.left(), rect.top(), rect.width(), rect.height());
|
||||
}
|
||||
}
|
||||
blur_.reset(blurManager_->getBackgroundEffect(window_->surface()));
|
||||
blur_->setBlurRegion(region.get());
|
||||
}
|
||||
|
||||
void WaylandInputWindow::updateScale() { window_->updateScale(); }
|
||||
|
||||
void WaylandInputWindow::resetPanel() { panelSurface_.reset(); }
|
||||
|
||||
void WaylandInputWindow::update(fcitx::InputContext *ic) {
|
||||
const auto oldVisible = visible();
|
||||
auto [width, height] = InputWindow::update(ic);
|
||||
CLASSICUI_DEBUG() << "Wayland Input Window visible:" << visible()
|
||||
<< " for IC program:"
|
||||
<< (ic ? ic->program() : std::string("-")) << " frontend:"
|
||||
<< (ic ? ic->frontendName() : std::string("-"));
|
||||
if (!oldVisible && !visible()) {
|
||||
CLASSICUI_DEBUG() << "Wayland Input Window has been hidden.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!visible()) {
|
||||
CLASSICUI_DEBUG() << "Hide Wayland Input Window.";
|
||||
hoverIndex_ = -1;
|
||||
window_->hide();
|
||||
repaintIC_.unwatch();
|
||||
panelSurface_.reset();
|
||||
panelSurfaceV2_.reset();
|
||||
blur_.reset();
|
||||
window_->destroyWindow();
|
||||
return;
|
||||
}
|
||||
|
||||
assert(!visible() || ic != nullptr);
|
||||
|
||||
CLASSICUI_DEBUG()
|
||||
<< "Wayland Input Window is visible, ensure surface is created.";
|
||||
initPanel();
|
||||
if (ic->frontendName() == "wayland_v2") {
|
||||
if (!panelSurfaceV2_ || ic != v2IC_.get()) {
|
||||
auto *waylandim = ui_->parent()->waylandim();
|
||||
if (!waylandim) {
|
||||
CLASSICUI_ERROR()
|
||||
<< "Failed to request waylandim addon, this should not "
|
||||
"happen since we have wayland_v2 input context.";
|
||||
return;
|
||||
}
|
||||
auto *im = waylandim->call<IWaylandIMModule::getInputMethodV2>(ic);
|
||||
if (!im) {
|
||||
CLASSICUI_ERROR()
|
||||
<< "Failed to request get zwp_input_method_v2 object, this "
|
||||
"should not happen since we have wayland_v2 input "
|
||||
"context.";
|
||||
return;
|
||||
}
|
||||
v2IC_ = ic->watch();
|
||||
panelSurfaceV2_.reset();
|
||||
panelSurfaceV2_.reset(im->getInputPopupSurface(window_->surface()));
|
||||
}
|
||||
} else if (ic->frontendName() == "wayland") {
|
||||
auto panel = ui_->display()->getGlobal<wayland::ZwpInputPanelV1>();
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
if (!panelSurface_) {
|
||||
panelSurface_.reset(
|
||||
panel->getInputPanelSurface(window_->surface()));
|
||||
panelSurface_->setOverlayPanel();
|
||||
}
|
||||
}
|
||||
if (!panelSurface_ && !panelSurfaceV2_) {
|
||||
CLASSICUI_DEBUG() << "No Panel surface available, return.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (width != window_->width() || height != window_->height()) {
|
||||
window_->resize(width, height);
|
||||
updateBlur();
|
||||
}
|
||||
|
||||
repaint();
|
||||
repaintIC_ = ic->watch();
|
||||
}
|
||||
|
||||
void WaylandInputWindow::repaint() {
|
||||
if (!visible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto *surface = window_->prerender()) {
|
||||
cairo_t *c = cairo_create(surface);
|
||||
auto surfaceWidth = cairo_image_surface_get_width(surface);
|
||||
auto surfaceHeight = cairo_image_surface_get_height(surface);
|
||||
cairo_surface_set_device_scale(
|
||||
cairo_get_target(c),
|
||||
window_->bufferScale() / WaylandWindow::ScaleDominatorF,
|
||||
window_->bufferScale() / WaylandWindow::ScaleDominatorF);
|
||||
paint(c,
|
||||
surfaceWidth * WaylandWindow::ScaleDominatorF /
|
||||
window_->bufferScale(),
|
||||
surfaceHeight * WaylandWindow::ScaleDominatorF /
|
||||
window_->bufferScale());
|
||||
cairo_destroy(c);
|
||||
window_->render();
|
||||
} else if (showAnimating_ || highlightAnimating_) {
|
||||
// No free buffer for this frame, try again on the next one.
|
||||
scheduleFrame();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2017-2017 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#ifndef _FCITX_UI_CLASSIC_WAYLANDINPUTWINDOW_H_
|
||||
#define _FCITX_UI_CLASSIC_WAYLANDINPUTWINDOW_H_
|
||||
|
||||
#include <memory>
|
||||
#include <wayland-util.h>
|
||||
#include "fcitx-utils/trackableobject.h"
|
||||
#include "fcitx/inputcontext.h"
|
||||
#include "ext_background_effect_manager_v1.h"
|
||||
#include "ext_background_effect_surface_v1.h"
|
||||
#include "inputwindow.h"
|
||||
#include "zwp_input_panel_surface_v1.h"
|
||||
#include "zwp_input_popup_surface_v2.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
class WaylandUI;
|
||||
class WaylandWindow;
|
||||
|
||||
class WaylandInputWindow : public InputWindow {
|
||||
public:
|
||||
WaylandInputWindow(WaylandUI *ui);
|
||||
|
||||
void initPanel();
|
||||
void resetPanel();
|
||||
void update(InputContext *ic);
|
||||
void repaint();
|
||||
void
|
||||
setBlurManager(std::shared_ptr<wayland::ExtBackgroundEffectManagerV1> blur);
|
||||
void updateScale();
|
||||
|
||||
private:
|
||||
void updateBlur();
|
||||
|
||||
WaylandUI *ui_;
|
||||
wl_fixed_t scroll_ = 0;
|
||||
std::unique_ptr<wayland::ZwpInputPanelSurfaceV1> panelSurface_;
|
||||
TrackableObjectReference<InputContext> v2IC_;
|
||||
std::unique_ptr<wayland::ZwpInputPopupSurfaceV2> panelSurfaceV2_;
|
||||
std::unique_ptr<WaylandWindow> window_;
|
||||
TrackableObjectReference<InputContext> repaintIC_;
|
||||
std::shared_ptr<wayland::ExtBackgroundEffectManagerV1> blurManager_;
|
||||
std::unique_ptr<wayland::ExtBackgroundEffectSurfaceV1> blur_;
|
||||
};
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
|
||||
#endif // _FCITX_UI_CLASSIC_WAYLANDINPUTWINDOW_H_
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020-2020 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
|
||||
#include "waylandpointer.h"
|
||||
#include <cstdint>
|
||||
#include <wayland-util.h>
|
||||
#include "waylandwindow.h"
|
||||
#include "wl_seat.h"
|
||||
#include "wl_surface.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
WaylandPointer::WaylandPointer(WaylandUI *ui, wayland::WlSeat *seat)
|
||||
: ui_(ui), display_(ui_->display()) {
|
||||
capConn_ = seat->capabilities().connect([this, seat](uint32_t caps) {
|
||||
if ((caps & WL_SEAT_CAPABILITY_POINTER) && !pointer_) {
|
||||
pointer_.reset(seat->getPointer());
|
||||
initPointer();
|
||||
} else if (!(caps & WL_SEAT_CAPABILITY_POINTER) && pointer_) {
|
||||
pointer_.reset();
|
||||
}
|
||||
|
||||
if ((caps & WL_SEAT_CAPABILITY_TOUCH) && !touch_) {
|
||||
touch_.reset(seat->getTouch());
|
||||
initTouch();
|
||||
} else if (!(caps & WL_SEAT_CAPABILITY_TOUCH) && touch_) {
|
||||
touch_.reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void WaylandPointer::initPointer() {
|
||||
pointer_->enter().connect([this](uint32_t serial,
|
||||
wayland::WlSurface *surface, wl_fixed_t sx,
|
||||
wl_fixed_t sy) {
|
||||
enterSerial_ = serial;
|
||||
cursor()->update();
|
||||
auto *window = static_cast<WaylandWindow *>(surface->userData());
|
||||
if (!window) {
|
||||
return;
|
||||
}
|
||||
pointerFocus_ = window->watch();
|
||||
pointerFocusX_ = wl_fixed_to_int(sx);
|
||||
pointerFocusY_ = wl_fixed_to_int(sy);
|
||||
});
|
||||
pointer_->leave().connect([this](uint32_t, wayland::WlSurface *surface) {
|
||||
if (auto *window = pointerFocus_.get()) {
|
||||
if (window->surface() == surface) {
|
||||
pointerFocus_.unwatch();
|
||||
window->leave()();
|
||||
}
|
||||
}
|
||||
});
|
||||
pointer_->motion().connect([this](uint32_t, wl_fixed_t sx, wl_fixed_t sy) {
|
||||
if (auto *window = pointerFocus_.get()) {
|
||||
pointerFocusX_ = wl_fixed_to_int(sx);
|
||||
pointerFocusY_ = wl_fixed_to_int(sy);
|
||||
window->hover()(pointerFocusX_, pointerFocusY_);
|
||||
}
|
||||
});
|
||||
pointer_->button().connect(
|
||||
[this](uint32_t, uint32_t, uint32_t button, uint32_t state) {
|
||||
if (auto *window = pointerFocus_.get()) {
|
||||
window->click()(pointerFocusX_, pointerFocusY_, button, state);
|
||||
}
|
||||
});
|
||||
pointer_->axis().connect([this](uint32_t, uint32_t axis, wl_fixed_t value) {
|
||||
if (auto *window = pointerFocus_.get()) {
|
||||
window->axis()(pointerFocusX_, pointerFocusY_, axis, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void WaylandPointer::initTouch() {
|
||||
touch_->down().connect([this](uint32_t, uint32_t,
|
||||
wayland::WlSurface *surface, int,
|
||||
wl_fixed_t sx, wl_fixed_t sy) {
|
||||
// TODO handle id for multiple touch
|
||||
auto *window = static_cast<WaylandWindow *>(surface->userData());
|
||||
if (!window) {
|
||||
return;
|
||||
}
|
||||
touchFocus_ = window->watch();
|
||||
touchFocusX_ = wl_fixed_to_int(sx);
|
||||
touchFocusY_ = wl_fixed_to_int(sy);
|
||||
window->touchDown()(touchFocusX_, touchFocusY_);
|
||||
});
|
||||
touch_->up().connect([this](uint32_t, uint32_t, int) {
|
||||
// TODO handle id for multiple touch
|
||||
if (auto *window = touchFocus_.get()) {
|
||||
window->touchUp()(touchFocusX_, touchFocusY_);
|
||||
touchFocus_.unwatch();
|
||||
window->leave()();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020-2020 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#ifndef _FCITX_UI_CLASSIC_WAYLANDPOINTER_H_
|
||||
#define _FCITX_UI_CLASSIC_WAYLANDPOINTER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include "fcitx-utils/signals.h"
|
||||
#include "fcitx-utils/trackableobject.h"
|
||||
#include "display.h"
|
||||
#include "waylandcursor.h"
|
||||
#include "wl_pointer.h"
|
||||
#include "wl_seat.h"
|
||||
#include "wl_touch.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
class WaylandUI;
|
||||
class WaylandWindow;
|
||||
|
||||
class WaylandPointer {
|
||||
public:
|
||||
WaylandPointer(WaylandUI *ui, wayland::WlSeat *seat);
|
||||
|
||||
auto ui() const { return ui_; }
|
||||
auto pointer() const { return pointer_.get(); }
|
||||
auto enterSerial() const { return enterSerial_; }
|
||||
|
||||
WaylandCursor *cursor() {
|
||||
if (!cursor_) {
|
||||
cursor_ = std::make_unique<WaylandCursor>(this);
|
||||
}
|
||||
return cursor_.get();
|
||||
}
|
||||
|
||||
private:
|
||||
void initPointer();
|
||||
void initTouch();
|
||||
WaylandUI *ui_;
|
||||
wayland::Display *display_;
|
||||
std::unique_ptr<wayland::WlPointer> pointer_;
|
||||
TrackableObjectReference<WaylandWindow> pointerFocus_;
|
||||
int pointerFocusX_ = 0, pointerFocusY_ = 0;
|
||||
std::unique_ptr<wayland::WlTouch> touch_;
|
||||
TrackableObjectReference<WaylandWindow> touchFocus_;
|
||||
int touchFocusX_ = 0, touchFocusY_ = 0;
|
||||
ScopedConnection capConn_;
|
||||
uint32_t enterSerial_ = 0;
|
||||
std::unique_ptr<WaylandCursor> cursor_;
|
||||
};
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
|
||||
#endif // _FCITX_UI_CLASSIC_WAYLANDPOINTER_H_
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2017-2017 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
|
||||
#include "waylandshmwindow.h"
|
||||
#include <cstdint>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <cairo.h>
|
||||
#include "fcitx-utils/eventloopinterface.h"
|
||||
#include "buffer.h"
|
||||
#include "common.h"
|
||||
#include "waylandui.h"
|
||||
#include "waylandwindow.h"
|
||||
#include "wl_shm.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
namespace {
|
||||
|
||||
void surfaceToBufferSize(unsigned int buffer_scale, uint32_t *width,
|
||||
uint32_t *height) {
|
||||
// round (size * scale), but all integer calculation.
|
||||
*width = (*width * buffer_scale + (WaylandWindow::ScaleDominator / 2)) /
|
||||
WaylandWindow::ScaleDominator;
|
||||
*height = (*height * buffer_scale + (WaylandWindow::ScaleDominator / 2)) /
|
||||
WaylandWindow::ScaleDominator;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
WaylandShmWindow::WaylandShmWindow(WaylandUI *ui)
|
||||
: WaylandWindow(ui), shm_(ui->display()->getGlobal<wayland::WlShm>()) {}
|
||||
|
||||
WaylandShmWindow::~WaylandShmWindow() {}
|
||||
|
||||
void WaylandShmWindow::destroyWindow() {
|
||||
buffers_.clear();
|
||||
buffer_ = nullptr;
|
||||
WaylandWindow::destroyWindow();
|
||||
}
|
||||
|
||||
void WaylandShmWindow::newBuffer(uint32_t width, uint32_t height) {
|
||||
if (!shm_) {
|
||||
return;
|
||||
}
|
||||
buffers_.emplace_back(std::make_unique<wayland::Buffer>(
|
||||
shm_.get(), width, height, WL_SHM_FORMAT_ARGB8888));
|
||||
buffers_.back()->rendered().connect([this]() {
|
||||
// Use defer event here, otherwise repaint may delete buffer and cause
|
||||
// problem.
|
||||
deferEvent_ = ui_->parent()->instance()->eventLoop().addDeferEvent(
|
||||
[this](EventSource *) {
|
||||
if (pending_) {
|
||||
pending_ = false;
|
||||
|
||||
CLASSICUI_DEBUG() << "Trigger repaint";
|
||||
repaint_();
|
||||
}
|
||||
deferEvent_.reset();
|
||||
return true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
cairo_surface_t *WaylandShmWindow::prerender() {
|
||||
// We use double buffer.
|
||||
decltype(buffers_)::iterator iter;
|
||||
for (iter = buffers_.begin(); iter != buffers_.end(); iter++) {
|
||||
CLASSICUI_DEBUG() << "Buffer state: " << (*iter).get() << " "
|
||||
<< (*iter)->busy();
|
||||
if (!(*iter)->busy()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t bufferWidth = width_;
|
||||
uint32_t bufferHeight = height_;
|
||||
surfaceToBufferSize(bufferScale(), &bufferWidth, &bufferHeight);
|
||||
|
||||
if (iter != buffers_.end() && ((*iter)->width() != bufferWidth ||
|
||||
(*iter)->height() != bufferHeight)) {
|
||||
buffers_.erase(iter);
|
||||
iter = buffers_.end();
|
||||
}
|
||||
|
||||
if (iter == buffers_.end() && buffers_.size() < 2) {
|
||||
newBuffer(bufferWidth, bufferHeight);
|
||||
if (!buffers_.empty()) {
|
||||
iter = std::prev(buffers_.end());
|
||||
}
|
||||
}
|
||||
|
||||
if (iter == buffers_.end()) {
|
||||
CLASSICUI_DEBUG() << "Couldn't find avail buffer.";
|
||||
pending_ = true;
|
||||
// All buffers are busy.
|
||||
buffer_ = nullptr;
|
||||
return nullptr;
|
||||
}
|
||||
pending_ = false;
|
||||
buffer_ = iter->get();
|
||||
|
||||
auto *cairoSurface = buffer_->cairoSurface();
|
||||
if (!cairoSurface) {
|
||||
buffer_ = nullptr;
|
||||
return nullptr;
|
||||
}
|
||||
return cairoSurface;
|
||||
}
|
||||
|
||||
void WaylandShmWindow::render() {
|
||||
if (!buffer_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewport_) {
|
||||
if (buffer_->attachToSurface(surface_.get(), 1)) {
|
||||
viewport_->setDestination(width_, height_);
|
||||
surface_->commit();
|
||||
}
|
||||
} else {
|
||||
if (buffer_->attachToSurface(surface_.get(), lastOutputScale_)) {
|
||||
surface_->commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandShmWindow::hide() {
|
||||
if (!surface_) {
|
||||
return;
|
||||
}
|
||||
surface_->attach(nullptr, 0, 0);
|
||||
surface_->commit();
|
||||
}
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2017-2017 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#ifndef _FCITX_UI_CLASSIC_WAYLANDSHMWINDOW_H_
|
||||
#define _FCITX_UI_CLASSIC_WAYLANDSHMWINDOW_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <cairo.h>
|
||||
#include "fcitx-utils/eventloopinterface.h"
|
||||
#include "buffer.h"
|
||||
#include "waylandui.h"
|
||||
#include "waylandwindow.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
class WaylandShmWindow : public WaylandWindow {
|
||||
public:
|
||||
WaylandShmWindow(WaylandUI *ui);
|
||||
~WaylandShmWindow();
|
||||
|
||||
void destroyWindow();
|
||||
cairo_surface_t *prerender() override;
|
||||
void render() override;
|
||||
void hide() override;
|
||||
|
||||
private:
|
||||
void newBuffer(uint32_t width, uint32_t height);
|
||||
|
||||
std::shared_ptr<wayland::WlShm> shm_;
|
||||
std::vector<std::unique_ptr<wayland::Buffer>> buffers_;
|
||||
// Pointer to the current buffer.
|
||||
wayland::Buffer *buffer_ = nullptr;
|
||||
bool pending_ = false;
|
||||
std::unique_ptr<EventSource> deferEvent_;
|
||||
};
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
|
||||
#endif // _FCITX_UI_CLASSIC_WAYLANDSHMWINDOW_H_
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2016-2016 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
|
||||
#include "waylandui.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include "fcitx/userinterface.h"
|
||||
#include "classicui.h"
|
||||
#include "common.h"
|
||||
#include "display.h"
|
||||
#include "ext_background_effect_manager_v1.h"
|
||||
#include "waylandcursortheme.h"
|
||||
#include "waylandinputwindow.h"
|
||||
#include "waylandpointer.h"
|
||||
#include "waylandshmwindow.h"
|
||||
#include "wl_compositor.h"
|
||||
#include "wl_seat.h"
|
||||
#include "wl_shm.h"
|
||||
#include "wp_fractional_scale_manager_v1.h"
|
||||
#include "wp_viewporter.h"
|
||||
#include "zwp_input_panel_v1.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
WaylandUI::WaylandUI(M3UI *parent, const std::string &name,
|
||||
wl_display *display)
|
||||
: UIInterface("wayland:" + name), parent_(parent),
|
||||
display_(
|
||||
static_cast<wayland::Display *>(wl_display_get_user_data(display))) {
|
||||
display_->requestGlobals<wayland::WlCompositor>();
|
||||
display_->requestGlobals<wayland::WlShm>();
|
||||
display_->requestGlobals<wayland::WlSeat>();
|
||||
display_->requestGlobals<wayland::ZwpInputPanelV1>();
|
||||
display_->requestGlobals<wayland::ExtBackgroundEffectManagerV1>();
|
||||
display_->requestGlobals<wayland::WpFractionalScaleManagerV1>();
|
||||
display_->requestGlobals<wayland::WpViewporter>();
|
||||
panelConn_ = display_->globalCreated().connect(
|
||||
[this](const std::string &name, const std::shared_ptr<void> &) {
|
||||
if (name == wayland::ZwpInputPanelV1::interface) {
|
||||
if (inputWindow_) {
|
||||
inputWindow_->initPanel();
|
||||
}
|
||||
} else if (name == wayland::WlCompositor::interface ||
|
||||
name == wayland::WlShm::interface) {
|
||||
setupInputWindow();
|
||||
} else if (name == wayland::WlSeat::interface) {
|
||||
auto seat = display_->getGlobal<wayland::WlSeat>();
|
||||
if (seat) {
|
||||
pointer_ =
|
||||
std::make_unique<WaylandPointer>(this, seat.get());
|
||||
}
|
||||
} else if (name ==
|
||||
wayland::ExtBackgroundEffectManagerV1::interface) {
|
||||
if (inputWindow_) {
|
||||
inputWindow_->setBlurManager(
|
||||
display_->getGlobal<
|
||||
wayland::ExtBackgroundEffectManagerV1>());
|
||||
}
|
||||
} else if (name == wayland::WpFractionalScaleManagerV1::interface ||
|
||||
name == wayland::WpViewporter::interface) {
|
||||
if (inputWindow_) {
|
||||
inputWindow_->updateScale();
|
||||
}
|
||||
}
|
||||
});
|
||||
panelRemovedConn_ = display_->globalRemoved().connect(
|
||||
[this](const std::string &name, const std::shared_ptr<void> &) {
|
||||
if (name == wayland::ZwpInputPanelV1::interface) {
|
||||
if (inputWindow_) {
|
||||
inputWindow_->resetPanel();
|
||||
}
|
||||
} else if (name ==
|
||||
wayland::ExtBackgroundEffectManagerV1::interface) {
|
||||
if (inputWindow_) {
|
||||
inputWindow_->setBlurManager(nullptr);
|
||||
}
|
||||
} else if (name == wayland::WpFractionalScaleManagerV1::interface ||
|
||||
name == wayland::WpViewporter::interface) {
|
||||
if (inputWindow_) {
|
||||
inputWindow_->updateScale();
|
||||
}
|
||||
}
|
||||
});
|
||||
auto seat = display_->getGlobal<wayland::WlSeat>();
|
||||
if (seat) {
|
||||
pointer_ = std::make_unique<WaylandPointer>(this, seat.get());
|
||||
}
|
||||
setupInputWindow();
|
||||
}
|
||||
|
||||
WaylandUI::~WaylandUI() {}
|
||||
|
||||
void WaylandUI::update(UserInterfaceComponent component,
|
||||
InputContext *inputContext) {
|
||||
if (inputWindow_ && component == UserInterfaceComponent::InputPanel) {
|
||||
CLASSICUI_DEBUG() << "Update Wayland Input Window";
|
||||
inputWindow_->update(inputContext);
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandUI::suspend() {
|
||||
if (!inputWindow_) {
|
||||
return;
|
||||
}
|
||||
inputWindow_->update(nullptr);
|
||||
}
|
||||
|
||||
void WaylandUI::resume() {
|
||||
CLASSICUI_DEBUG() << "Resume WaylandUI display name:" << display_;
|
||||
CLASSICUI_DEBUG() << "Wayland Input window is initialized:"
|
||||
<< !!inputWindow_;
|
||||
}
|
||||
|
||||
void WaylandUI::setupInputWindow() {
|
||||
if (inputWindow_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unable to draw window.
|
||||
if (!display_->getGlobal<wayland::WlShm>()) {
|
||||
return;
|
||||
}
|
||||
// Unable to create surface.
|
||||
if (!display_->getGlobal<wayland::WlCompositor>()) {
|
||||
return;
|
||||
}
|
||||
|
||||
cursorTheme_ = std::make_unique<WaylandCursorTheme>(this);
|
||||
inputWindow_ = std::make_unique<WaylandInputWindow>(this);
|
||||
inputWindow_->initPanel();
|
||||
inputWindow_->setBlurManager(
|
||||
display_->getGlobal<wayland::ExtBackgroundEffectManagerV1>());
|
||||
}
|
||||
|
||||
std::unique_ptr<WaylandWindow> WaylandUI::newWindow() {
|
||||
return std::make_unique<WaylandShmWindow>(this);
|
||||
}
|
||||
} // namespace fcitx::m3ui
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2016-2016 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#ifndef _FCITX_UI_CLASSIC_WAYLANDUI_H_
|
||||
#define _FCITX_UI_CLASSIC_WAYLANDUI_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include "fcitx-utils/eventloopinterface.h"
|
||||
#include "fcitx-utils/signals.h"
|
||||
#include "fcitx/userinterface.h"
|
||||
#include "classicui.h"
|
||||
#include "display.h"
|
||||
#include "waylandcursortheme.h"
|
||||
#include "waylandpointer.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
class WaylandWindow;
|
||||
class WaylandInputWindow;
|
||||
|
||||
class WaylandUI : public UIInterface {
|
||||
public:
|
||||
WaylandUI(M3UI *parent, const std::string &name, wl_display *display);
|
||||
~WaylandUI();
|
||||
|
||||
M3UI *parent() const { return parent_; }
|
||||
wayland::Display *display() const { return display_; }
|
||||
void update(UserInterfaceComponent component,
|
||||
InputContext *inputContext) override;
|
||||
void suspend() override;
|
||||
void resume() override;
|
||||
void setEnableTray(bool /*unused*/) override {}
|
||||
|
||||
std::unique_ptr<WaylandWindow> newWindow();
|
||||
|
||||
WaylandCursorTheme *cursorTheme() { return cursorTheme_.get(); }
|
||||
|
||||
private:
|
||||
void setupInputWindow();
|
||||
|
||||
M3UI *parent_;
|
||||
wayland::Display *display_;
|
||||
ScopedConnection panelConn_, panelRemovedConn_;
|
||||
std::unique_ptr<WaylandCursorTheme> cursorTheme_;
|
||||
std::unique_ptr<WaylandPointer> pointer_;
|
||||
std::unique_ptr<WaylandInputWindow> inputWindow_;
|
||||
std::unique_ptr<EventSource> defer_;
|
||||
};
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
|
||||
#endif // _FCITX_UI_CLASSIC_WAYLANDUI_H_
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2016-2016 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
|
||||
#include "waylandwindow.h"
|
||||
#include <cstdint>
|
||||
#include "fcitx-utils/eventloopinterface.h"
|
||||
#include "waylandui.h"
|
||||
#include "wl_compositor.h"
|
||||
#include "wl_output.h"
|
||||
#include "wl_surface.h"
|
||||
#include "wp_fractional_scale_manager_v1.h"
|
||||
#include "wp_viewporter.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
WaylandWindow::WaylandWindow(WaylandUI *ui) : ui_(ui) {}
|
||||
|
||||
WaylandWindow::~WaylandWindow() {}
|
||||
|
||||
void WaylandWindow::createWindow() {
|
||||
auto compositor = ui_->display()->getGlobal<wayland::WlCompositor>();
|
||||
if (!compositor) {
|
||||
return;
|
||||
}
|
||||
surface_.reset(compositor->createSurface());
|
||||
surface_->setUserData(this);
|
||||
updateScale();
|
||||
|
||||
enterConn_ = surface_->enter().connect([this](wayland::WlOutput *output) {
|
||||
const auto *info = ui_->display()->outputInformation(output);
|
||||
if (!info) {
|
||||
return;
|
||||
}
|
||||
if (setScaleAndTransform(info->scale(), info->transform())) {
|
||||
scheduleRepaint();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void WaylandWindow::destroyWindow() {
|
||||
resetFractionalScale();
|
||||
surface_.reset();
|
||||
enterConn_.disconnect();
|
||||
}
|
||||
|
||||
void WaylandWindow::resetFractionalScale() {
|
||||
viewport_.reset();
|
||||
fractionalScale_.reset();
|
||||
viewporter_.reset();
|
||||
fractionalScaleManager_.reset();
|
||||
}
|
||||
|
||||
void WaylandWindow::scheduleRepaint() {
|
||||
repaintEvent_ = ui_->parent()->instance()->eventLoop().addDeferEvent(
|
||||
[this](EventSource *) {
|
||||
repaint_();
|
||||
repaintEvent_.reset();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void WaylandWindow::updateScale() {
|
||||
if (!surface_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!*ui_->parent()->config().fractionalScale) {
|
||||
resetFractionalScale();
|
||||
return;
|
||||
}
|
||||
|
||||
viewporter_ = ui_->display()->getGlobal<wayland::WpViewporter>();
|
||||
fractionalScaleManager_ =
|
||||
ui_->display()->getGlobal<wayland::WpFractionalScaleManagerV1>();
|
||||
if (viewporter_ && fractionalScaleManager_) {
|
||||
viewport_.reset(viewporter_->getViewport(surface_.get()));
|
||||
fractionalScale_.reset(
|
||||
fractionalScaleManager_->getFractionalScale(surface_.get()));
|
||||
}
|
||||
|
||||
if (viewport_ && fractionalScale_) {
|
||||
fractionalScale_->preferredScale().connect([this](uint32_t scale) {
|
||||
if (scale != lastFractionalScale_) {
|
||||
lastFractionalScale_ = scale;
|
||||
scheduleRepaint();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resetFractionalScale();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2016-2016 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#ifndef _FCITX_UI_CLASSIC_WAYLANDWINDOW_H_
|
||||
#define _FCITX_UI_CLASSIC_WAYLANDWINDOW_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <wayland-client-protocol.h>
|
||||
#include <wayland-util.h>
|
||||
#include "fcitx-utils/eventloopinterface.h"
|
||||
#include "fcitx-utils/rect.h"
|
||||
#include "fcitx-utils/signals.h"
|
||||
#include "fcitx-utils/trackableobject.h"
|
||||
#include "waylandui.h"
|
||||
#include "window.h"
|
||||
#include "wp_fractional_scale_manager_v1.h"
|
||||
#include "wp_fractional_scale_v1.h"
|
||||
#include "wp_viewport.h"
|
||||
#include "wp_viewporter.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
class WaylandWindow : public Window, public TrackableObject<WaylandWindow> {
|
||||
public:
|
||||
static constexpr unsigned int ScaleDominator = 120;
|
||||
static constexpr double ScaleDominatorF = ScaleDominator;
|
||||
WaylandWindow(WaylandUI *ui);
|
||||
~WaylandWindow();
|
||||
|
||||
virtual void createWindow();
|
||||
void destroyWindow();
|
||||
virtual void hide() = 0;
|
||||
|
||||
unsigned int bufferScale() const {
|
||||
return viewport_ ? lastFractionalScale_
|
||||
: lastOutputScale_ * ScaleDominator;
|
||||
}
|
||||
int32_t outputScale() const { return lastOutputScale_; }
|
||||
wl_output_transform transform() const { return transform_; }
|
||||
bool setScaleAndTransform(int32_t scale, wl_output_transform transform) {
|
||||
if (lastOutputScale_ != scale || transform_ != transform) {
|
||||
lastOutputScale_ = scale;
|
||||
transform_ = transform;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
wayland::WlSurface *surface() { return surface_.get(); }
|
||||
|
||||
auto &repaint() { return repaint_; }
|
||||
auto &hover() { return hover_; }
|
||||
auto &click() { return click_; }
|
||||
auto &axis() { return axis_; }
|
||||
auto &leave() { return leave_; }
|
||||
|
||||
auto &touchDown() { return touchDown_; }
|
||||
auto &touchUp() { return touchUp_; }
|
||||
|
||||
void updateScale();
|
||||
|
||||
protected:
|
||||
WaylandUI *ui_;
|
||||
std::unique_ptr<wayland::WlSurface> surface_;
|
||||
fcitx::ScopedConnection enterConn_;
|
||||
Signal<void()> repaint_;
|
||||
Signal<void(int, int)> hover_;
|
||||
Signal<void(int, int, uint32_t, uint32_t)> click_;
|
||||
Signal<void(int, int, uint32_t, wl_fixed_t)> axis_;
|
||||
Signal<void()> leave_;
|
||||
|
||||
Signal<void(int, int)> touchDown_;
|
||||
Signal<void(int, int)> touchUp_;
|
||||
|
||||
Rect serverAllocation_;
|
||||
Rect allocation_;
|
||||
|
||||
int32_t lastOutputScale_ = 1;
|
||||
unsigned int lastFractionalScale_ = ScaleDominator;
|
||||
wl_output_transform transform_ = WL_OUTPUT_TRANSFORM_NORMAL;
|
||||
|
||||
std::shared_ptr<wayland::WpViewporter> viewporter_;
|
||||
std::shared_ptr<wayland::WpFractionalScaleManagerV1>
|
||||
fractionalScaleManager_;
|
||||
|
||||
std::unique_ptr<wayland::WpViewport> viewport_;
|
||||
std::unique_ptr<wayland::WpFractionalScaleV1> fractionalScale_;
|
||||
std::unique_ptr<EventSource> repaintEvent_;
|
||||
|
||||
private:
|
||||
void resetFractionalScale();
|
||||
// Avoid repaint being done too much.
|
||||
// For input window, since it will re-created frequently, the initial scale
|
||||
// might be wrong. To avoid show blurry 1x on scaled monitor, we always use
|
||||
// the last value. But just "upon" window is shown on screen, more than one
|
||||
// new scale might arrive. Do not repaint upon every scale change.
|
||||
void scheduleRepaint();
|
||||
};
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
|
||||
#endif // _FCITX_UI_CLASSIC_WAYLANDWINDOW_H_
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2016-2016 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
|
||||
#include "window.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
Window::Window() = default;
|
||||
|
||||
void Window::resize(unsigned int width, unsigned int height) {
|
||||
width_ = width;
|
||||
height_ = height;
|
||||
}
|
||||
} // namespace fcitx::m3ui
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2016-2016 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#ifndef _FCITX_UI_CLASSIC_WINDOW_H_
|
||||
#define _FCITX_UI_CLASSIC_WINDOW_H_
|
||||
|
||||
#include <cairo.h>
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
class Window {
|
||||
public:
|
||||
Window();
|
||||
virtual ~Window() = default;
|
||||
|
||||
int width() const { return width_; }
|
||||
int height() const { return height_; }
|
||||
virtual void resize(unsigned int width, unsigned int height);
|
||||
|
||||
virtual cairo_surface_t *prerender() = 0;
|
||||
virtual void render() = 0;
|
||||
|
||||
protected:
|
||||
unsigned int width_ = 100;
|
||||
unsigned int height_ = 100;
|
||||
};
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
|
||||
#endif // _FCITX_UI_CLASSIC_WINDOW_H_
|
||||
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2017-2017 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
|
||||
#include "xcbinputwindow.h"
|
||||
#include <unistd.h>
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <cairo.h>
|
||||
#include <xcb/xcb.h>
|
||||
#include <xcb/xcb_aux.h>
|
||||
#include <xcb/xcb_ewmh.h>
|
||||
#include <xcb/xcb_icccm.h>
|
||||
#include <xcb/xproto.h>
|
||||
#include "fcitx-utils/rect.h"
|
||||
#include "fcitx/inputcontext.h"
|
||||
#include "inputwindow.h"
|
||||
#include "theme.h"
|
||||
#include "xcb_public.h"
|
||||
#include "xcbui.h"
|
||||
#include "xcbwindow.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
XCBInputWindow::XCBInputWindow(XCBUI *ui)
|
||||
: XCBWindow(ui), InputWindow(ui->parent()),
|
||||
atomBlur_(ui_->parent()->xcb()->call<IXCBModule::atom>(
|
||||
ui_->displayName(), "_KDE_NET_WM_BLUR_BEHIND_REGION", false)) {}
|
||||
|
||||
void XCBInputWindow::postCreateWindow() {
|
||||
if (ui_->ewmh()->_NET_WM_WINDOW_TYPE_COMBO &&
|
||||
ui_->ewmh()->_NET_WM_WINDOW_TYPE) {
|
||||
xcb_ewmh_set_wm_window_type(ui_->ewmh(), wid_, 1,
|
||||
&ui_->ewmh()->_NET_WM_WINDOW_TYPE_COMBO);
|
||||
}
|
||||
|
||||
if (ui_->ewmh()->_NET_WM_PID) {
|
||||
xcb_ewmh_set_wm_pid(ui_->ewmh(), wid_, getpid());
|
||||
}
|
||||
|
||||
const char name[] = "Fcitx5 Input Window";
|
||||
xcb_icccm_set_wm_name(ui_->connection(), wid_, XCB_ATOM_STRING, 8,
|
||||
sizeof(name) - 1, name);
|
||||
const char klass[] = "fcitx\0fcitx";
|
||||
xcb_icccm_set_wm_class(ui_->connection(), wid_, sizeof(klass) - 1, klass);
|
||||
addEventMaskToWindow(
|
||||
ui_->connection(), wid_,
|
||||
XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE |
|
||||
XCB_EVENT_MASK_POINTER_MOTION | XCB_EVENT_MASK_EXPOSURE |
|
||||
XCB_EVENT_MASK_LEAVE_WINDOW);
|
||||
}
|
||||
|
||||
const Rect *XCBInputWindow::getClosestScreen(const Rect &cursorRect) const {
|
||||
const Rect *closestScreen = nullptr;
|
||||
|
||||
int shortestDistance = INT_MAX;
|
||||
for (const auto &rect : ui_->screenRects()) {
|
||||
int thisDistance =
|
||||
rect.first.distance(cursorRect.left(), cursorRect.top());
|
||||
if (thisDistance < shortestDistance) {
|
||||
shortestDistance = thisDistance;
|
||||
closestScreen = &rect.first;
|
||||
}
|
||||
}
|
||||
|
||||
return closestScreen;
|
||||
}
|
||||
|
||||
int XCBInputWindow::calculatePositionX(const Rect &cursorRect,
|
||||
const Rect *closestScreen) const {
|
||||
// TODO: RTL support.
|
||||
int x = cursorRect.left();
|
||||
|
||||
auto &theme = parent_->theme();
|
||||
int leftSW =
|
||||
physicalFromLogical(theme.inputPanel->shadowMargin->marginLeft.value());
|
||||
int rightSW = physicalFromLogical(
|
||||
theme.inputPanel->shadowMargin->marginRight.value());
|
||||
|
||||
int actualWidth = physicalWidth_ - leftSW - rightSW;
|
||||
actualWidth = actualWidth <= 0 ? physicalWidth_ : actualWidth;
|
||||
|
||||
if (closestScreen != nullptr) {
|
||||
int newX = std::max(x, closestScreen->left());
|
||||
|
||||
if (newX + actualWidth > closestScreen->right()) {
|
||||
newX = closestScreen->right() - actualWidth;
|
||||
}
|
||||
|
||||
x = std::max(newX, closestScreen->left());
|
||||
}
|
||||
|
||||
// exclude shadow border width
|
||||
x -= leftSW;
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
int XCBInputWindow::calculatePositionY(const Rect &cursorRect,
|
||||
const Rect *closestScreen) const {
|
||||
// TODO: RTL support.
|
||||
int y = cursorRect.top();
|
||||
|
||||
auto &theme = parent_->theme();
|
||||
int topSW =
|
||||
physicalFromLogical(theme.inputPanel->shadowMargin->marginTop.value());
|
||||
int bottomSW = physicalFromLogical(
|
||||
theme.inputPanel->shadowMargin->marginBottom.value());
|
||||
|
||||
int actualHeight = physicalHeight_ - topSW - bottomSW;
|
||||
actualHeight = actualHeight <= 0 ? physicalHeight_ : actualHeight;
|
||||
|
||||
if (closestScreen != nullptr) {
|
||||
int h = cursorRect.height();
|
||||
int newY;
|
||||
if (y < closestScreen->top()) {
|
||||
newY = closestScreen->top();
|
||||
} else {
|
||||
newY = y + (h ? h : physicalFromLogical(10));
|
||||
}
|
||||
|
||||
// Try flip y.
|
||||
if (newY + actualHeight > closestScreen->bottom()) {
|
||||
if (newY > closestScreen->bottom()) {
|
||||
newY = closestScreen->bottom() - actualHeight -
|
||||
physicalFromLogical(40);
|
||||
} else { /* better position the window */
|
||||
newY = newY - actualHeight -
|
||||
((h == 0) ? physicalFromLogical(40) : h);
|
||||
}
|
||||
|
||||
// If after flip, top is out of the screen, we still prefer the top
|
||||
// edge to be always with in screen.
|
||||
if (newY < closestScreen->top()) {
|
||||
newY = closestScreen->top();
|
||||
}
|
||||
}
|
||||
|
||||
y = newY;
|
||||
}
|
||||
|
||||
// exclude shadow border width
|
||||
y -= topSW;
|
||||
|
||||
return y;
|
||||
}
|
||||
|
||||
void XCBInputWindow::updatePosition(InputContext *inputContext) {
|
||||
if (!visible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Rect &cursorRect = inputContext->cursorRect();
|
||||
const Rect *closestScreen = getClosestScreen(cursorRect);
|
||||
xcb_params_configure_window_t wc;
|
||||
wc.x = calculatePositionX(cursorRect, closestScreen);
|
||||
wc.y = calculatePositionY(cursorRect, closestScreen);
|
||||
wc.stack_mode = XCB_STACK_MODE_ABOVE;
|
||||
xcb_aux_configure_window(ui_->connection(), wid_,
|
||||
XCB_CONFIG_WINDOW_STACK_MODE |
|
||||
XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y,
|
||||
&wc);
|
||||
}
|
||||
|
||||
void XCBInputWindow::updateDPI(InputContext *inputContext) {
|
||||
auto dpi = ui_->dpiByPosition(inputContext->cursorRect().left(),
|
||||
inputContext->cursorRect().top());
|
||||
setScale(scaleForDPI(dpi));
|
||||
}
|
||||
|
||||
void XCBInputWindow::update(InputContext *inputContext) {
|
||||
if (!wid_) {
|
||||
return;
|
||||
}
|
||||
auto oldVisible = visible();
|
||||
if (inputContext) {
|
||||
updateDPI(inputContext);
|
||||
}
|
||||
auto [width, height] = InputWindow::update(inputContext);
|
||||
if (!visible()) {
|
||||
if (oldVisible) {
|
||||
xcb_unmap_window(ui_->connection(), wid_);
|
||||
hoverIndex_ = -1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
auto oldPhysicalWidth = physicalWidth_;
|
||||
auto oldPhysicalHeight = physicalHeight_;
|
||||
resize(width, height);
|
||||
if (physicalWidth_ != oldPhysicalWidth ||
|
||||
physicalHeight_ != oldPhysicalHeight) {
|
||||
if (atomBlur_) {
|
||||
Rect logicalRect(0, 0, width, height);
|
||||
shrink(logicalRect, *ui_->parent()->theme().inputPanel->blurMargin);
|
||||
if (!*ui_->parent()->theme().inputPanel->enableBlur ||
|
||||
logicalRect.isEmpty()) {
|
||||
xcb_delete_property(ui_->connection(), wid_, atomBlur_);
|
||||
} else {
|
||||
auto rect = physicalFromLogical(logicalRect);
|
||||
std::vector<uint32_t> data;
|
||||
if (ui_->parent()->theme().inputPanel->blurMask->empty()) {
|
||||
data.push_back(rect.left());
|
||||
data.push_back(rect.top());
|
||||
data.push_back(rect.width());
|
||||
data.push_back(rect.height());
|
||||
xcb_change_property(ui_->connection(),
|
||||
XCB_PROP_MODE_REPLACE, wid_, atomBlur_,
|
||||
XCB_ATOM_CARDINAL, 32, data.size(),
|
||||
data.data());
|
||||
} else {
|
||||
auto region = parent_->theme().mask(
|
||||
parent_->theme().maskConfig(), width, height);
|
||||
for (const auto &rect : region) {
|
||||
auto physicalRect = physicalFromLogical(rect);
|
||||
data.push_back(physicalRect.left());
|
||||
data.push_back(physicalRect.top());
|
||||
data.push_back(physicalRect.width());
|
||||
data.push_back(physicalRect.height());
|
||||
}
|
||||
xcb_change_property(ui_->connection(),
|
||||
XCB_PROP_MODE_REPLACE, wid_, atomBlur_,
|
||||
XCB_ATOM_CARDINAL, 32, data.size(),
|
||||
data.data());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updatePosition(inputContext);
|
||||
repaint();
|
||||
if (!oldVisible) {
|
||||
xcb_map_window(ui_->connection(), wid_);
|
||||
}
|
||||
}
|
||||
|
||||
bool XCBInputWindow::filterEvent(xcb_generic_event_t *event) {
|
||||
uint8_t response_type = event->response_type & ~0x80;
|
||||
switch (response_type) {
|
||||
case XCB_EXPOSE: {
|
||||
auto *expose = reinterpret_cast<xcb_expose_event_t *>(event);
|
||||
if (expose->window == wid_) {
|
||||
repaint();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case XCB_BUTTON_PRESS: {
|
||||
auto *buttonPress = reinterpret_cast<xcb_button_press_event_t *>(event);
|
||||
if (buttonPress->event != wid_) {
|
||||
break;
|
||||
}
|
||||
if (buttonPress->detail == XCB_BUTTON_INDEX_1) {
|
||||
click(logicalFromPhysical(buttonPress->event_x),
|
||||
logicalFromPhysical(buttonPress->event_y));
|
||||
} else if (buttonPress->detail == XCB_BUTTON_INDEX_3) {
|
||||
rightClick(logicalFromPhysical(buttonPress->event_x),
|
||||
logicalFromPhysical(buttonPress->event_y));
|
||||
} else if (buttonPress->detail == XCB_BUTTON_INDEX_4) {
|
||||
wheel(/*up=*/true);
|
||||
} else if (buttonPress->detail == XCB_BUTTON_INDEX_5) {
|
||||
wheel(/*up=*/false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case XCB_MOTION_NOTIFY: {
|
||||
auto *motion = reinterpret_cast<xcb_motion_notify_event_t *>(event);
|
||||
if (motion->event == wid_) {
|
||||
if (hover(logicalFromPhysical(motion->event_x),
|
||||
logicalFromPhysical(motion->event_y))) {
|
||||
repaint();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case XCB_LEAVE_NOTIFY: {
|
||||
auto *leave = reinterpret_cast<xcb_leave_notify_event_t *>(event);
|
||||
if (leave->event == wid_) {
|
||||
if (hover(-1, -1)) {
|
||||
repaint();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void XCBInputWindow::repaint() {
|
||||
if (!visible()) {
|
||||
return;
|
||||
}
|
||||
if (auto *surface = prerender()) {
|
||||
cairo_t *c = cairo_create(surface);
|
||||
auto surfaceWidth = cairo_image_surface_get_width(surface);
|
||||
auto surfaceHeight = cairo_image_surface_get_height(surface);
|
||||
paint(c, surfaceWidth / scale_, surfaceHeight / scale_);
|
||||
cairo_destroy(c);
|
||||
render();
|
||||
} else if (showAnimating_ || highlightAnimating_) {
|
||||
// No free buffer for this frame, try again on the next one.
|
||||
scheduleFrame();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2017-2017 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#ifndef _FCITX_UI_CLASSIC_XCBINPUTWINDOW_H_
|
||||
#define _FCITX_UI_CLASSIC_XCBINPUTWINDOW_H_
|
||||
|
||||
#include <xcb/xcb.h>
|
||||
#include <xcb/xproto.h>
|
||||
#include "fcitx-utils/rect.h"
|
||||
#include "fcitx/inputcontext.h"
|
||||
#include "inputwindow.h"
|
||||
#include "xcbui.h"
|
||||
#include "xcbwindow.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
class XCBInputWindow : public XCBWindow, protected InputWindow {
|
||||
public:
|
||||
XCBInputWindow(XCBUI *ui);
|
||||
|
||||
void postCreateWindow() override;
|
||||
void update(InputContext *inputContext);
|
||||
void updatePosition(InputContext *inputContext);
|
||||
|
||||
bool filterEvent(xcb_generic_event_t *event) override;
|
||||
|
||||
void updateDPI(InputContext *inputContext);
|
||||
|
||||
private:
|
||||
void repaint();
|
||||
const Rect *getClosestScreen(const Rect &cursorRect) const;
|
||||
int calculatePositionX(const Rect &cursorRect,
|
||||
const Rect *closestScreen) const;
|
||||
int calculatePositionY(const Rect &cursorRect,
|
||||
const Rect *closestScreen) const;
|
||||
|
||||
xcb_atom_t atomBlur_;
|
||||
};
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
|
||||
#endif // _FCITX_UI_CLASSIC_XCBINPUTWINDOW_H_
|
||||
@@ -0,0 +1,862 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2017-2017 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#include "xcbmenu.h"
|
||||
#include <unistd.h>
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <cairo.h>
|
||||
#include <pango/pango-context.h>
|
||||
#include <pango/pango-font.h>
|
||||
#include <pango/pango-fontmap.h>
|
||||
#include <pango/pango-layout.h>
|
||||
#include <pango/pangocairo.h>
|
||||
#include <xcb/xcb.h>
|
||||
#include <xcb/xcb_aux.h>
|
||||
#include <xcb/xcb_ewmh.h>
|
||||
#include <xcb/xcb_icccm.h>
|
||||
#include <xcb/xcb_keysyms.h>
|
||||
#include <xcb/xproto.h>
|
||||
#include <yoga/YGEnums.h>
|
||||
#include <yoga/YGNode.h>
|
||||
#include <yoga/YGNodeLayout.h>
|
||||
#include <yoga/YGNodeStyle.h>
|
||||
#include <yoga/YGValue.h>
|
||||
#include "fcitx-utils/color.h"
|
||||
#include "fcitx-utils/connectableobject.h"
|
||||
#include "fcitx-utils/eventloopinterface.h"
|
||||
#include "fcitx-utils/log.h"
|
||||
#include "fcitx-utils/rect.h"
|
||||
#include "fcitx-utils/signals.h"
|
||||
#include "fcitx-utils/trackableobject.h"
|
||||
#include "fcitx/action.h"
|
||||
#include "fcitx/inputcontext.h"
|
||||
#include "fcitx/menu.h"
|
||||
#include "fcitx/userinterfacemanager.h"
|
||||
#include "common.h"
|
||||
#include "theme.h"
|
||||
#include "xcb_public.h"
|
||||
#include "xcbui.h"
|
||||
#include "xcbwindow.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
XCBMenu::XCBMenu(XCBUI *ui, MenuPool *pool, Menu *menu)
|
||||
: XCBWindow(ui), pool_(pool), menu_(menu),
|
||||
atomBlur_(ui_->parent()->xcb()->call<IXCBModule::atom>(
|
||||
ui_->displayName(), "_KDE_NET_WM_BLUR_BEHIND_REGION", false)) {
|
||||
fontMap_.reset(pango_cairo_font_map_new());
|
||||
context_.reset(pango_font_map_create_context(fontMap_.get()));
|
||||
rootNode_.reset(YGNodeNew());
|
||||
if (auto *ic = ui_->parent()->instance()->mostRecentInputContext()) {
|
||||
lastRelevantIc_ = ic->watch();
|
||||
}
|
||||
createWindow(ui_->visualId());
|
||||
}
|
||||
|
||||
XCBMenu::~XCBMenu() {}
|
||||
|
||||
bool XCBMenu::filterEvent(xcb_generic_event_t *event) {
|
||||
uint8_t response_type = event->response_type & ~0x80;
|
||||
switch (response_type) {
|
||||
case XCB_EXPOSE: {
|
||||
auto *expose = reinterpret_cast<xcb_expose_event_t *>(event);
|
||||
if (expose->window == wid_) {
|
||||
CLASSICUI_DEBUG() << "Menu received expose event";
|
||||
update();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case XCB_FOCUS_IN: {
|
||||
auto *focusIn = reinterpret_cast<xcb_focus_in_event_t *>(event);
|
||||
if (focusIn->event == wid_) {
|
||||
if (focusIn->detail == XCB_NOTIFY_DETAIL_POINTER) {
|
||||
return true;
|
||||
}
|
||||
// FCITX_INFO() << this << " Focus in";
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case XCB_FOCUS_OUT: {
|
||||
auto *focusOut = reinterpret_cast<xcb_focus_out_event_t *>(event);
|
||||
if (focusOut->event == wid_) {
|
||||
if (focusOut->detail == XCB_NOTIFY_DETAIL_POINTER) {
|
||||
return true;
|
||||
}
|
||||
// FCITX_INFO() << this << " Focus out " << subMenuIndex_;
|
||||
if (subMenuIndex_ < 0) {
|
||||
hideChilds();
|
||||
hide();
|
||||
hideParents();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case XCB_BUTTON_PRESS: {
|
||||
auto *buttonPress = reinterpret_cast<xcb_button_press_event_t *>(event);
|
||||
if (buttonPress->event != wid_) {
|
||||
break;
|
||||
}
|
||||
if (buttonPress->detail != XCB_BUTTON_INDEX_1) {
|
||||
hideAll();
|
||||
return true;
|
||||
}
|
||||
if (auto *menu =
|
||||
childByPosition(buttonPress->root_x, buttonPress->root_y)) {
|
||||
menu->handleButtonPress(
|
||||
menu->logicalFromPhysical(buttonPress->root_x - menu->x_),
|
||||
menu->logicalFromPhysical(buttonPress->root_y - menu->y_));
|
||||
} else {
|
||||
hideAll();
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case XCB_MOTION_NOTIFY: {
|
||||
auto *motion = reinterpret_cast<xcb_motion_notify_event_t *>(event);
|
||||
if (motion->event != wid_) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (auto *menu = childByPosition(motion->root_x, motion->root_y)) {
|
||||
menu->handleMotionNotify(
|
||||
menu->logicalFromPhysical(motion->root_x - menu->x_),
|
||||
menu->logicalFromPhysical(motion->root_y - menu->y_));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case XCB_ENTER_NOTIFY: {
|
||||
auto *enter = reinterpret_cast<xcb_enter_notify_event_t *>(event);
|
||||
if (enter->event != wid_) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (auto *menu = childByPosition(enter->root_x, enter->root_y)) {
|
||||
menu->hasMouse_ = true;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case XCB_LEAVE_NOTIFY: {
|
||||
auto *leave = reinterpret_cast<xcb_leave_notify_event_t *>(event);
|
||||
if (leave->event != wid_) {
|
||||
break;
|
||||
}
|
||||
if (auto *menu = childByPosition(leave->root_x, leave->root_y)) {
|
||||
menu->hasMouse_ = false;
|
||||
menu->setHoveredIndex(-1);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case XCB_KEY_PRESS: {
|
||||
auto *key = reinterpret_cast<xcb_key_press_event_t *>(event);
|
||||
if (key->event == wid_) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void XCBMenu::handleButtonPress(int eventX, int eventY) {
|
||||
for (size_t i = 0; i < items_.size(); i++) {
|
||||
if (items_[i].isSeparator_ ||
|
||||
!items_[i].region_.contains(eventX, eventY)) {
|
||||
continue;
|
||||
}
|
||||
if (items_[i].hasSubMenu_) {
|
||||
return;
|
||||
}
|
||||
// Check if actions is still good.
|
||||
auto actions = menu_->actions();
|
||||
if (i >= actions.size()) {
|
||||
break;
|
||||
}
|
||||
auto *ic = lastRelevantIc();
|
||||
if (!ic) {
|
||||
ic = ui_->parent()
|
||||
->instance()
|
||||
->inputContextManager()
|
||||
.dummyInputContext();
|
||||
}
|
||||
|
||||
auto id = actions[i]->id();
|
||||
// Why we need to delay the event, because we
|
||||
// want to make ic has focus.
|
||||
activateTimer_ = ui_->parent()->instance()->eventLoop().addTimeEvent(
|
||||
CLOCK_MONOTONIC, now(CLOCK_MONOTONIC) + 30000, 0,
|
||||
[this, that = this->watch(), icRef = ic->watch(),
|
||||
id](EventSourceTime *, uint64_t) {
|
||||
if (!that.isValid()) {
|
||||
return true;
|
||||
}
|
||||
// FCITX_INFO() << "Timer Triggered";
|
||||
if (auto *ic = icRef.get()) {
|
||||
auto *action = ui_->parent()
|
||||
->instance()
|
||||
->userInterfaceManager()
|
||||
.lookupActionById(id);
|
||||
if (action) {
|
||||
action->activate(ic);
|
||||
}
|
||||
}
|
||||
activateTimer_.reset();
|
||||
return true;
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
hideAll();
|
||||
}
|
||||
|
||||
void XCBMenu::handleMotionNotify(int eventX, int eventY) {
|
||||
for (size_t i = 0; i < items_.size(); i++) {
|
||||
if (!items_[i].isSeparator_ &&
|
||||
items_[i].region_.contains(eventX, eventY)) {
|
||||
setHoveredIndex(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void XCBMenu::hide() {
|
||||
if (!visible_) {
|
||||
return;
|
||||
}
|
||||
// FCITX_INFO() << "Hide " << this;
|
||||
visible_ = false;
|
||||
setParent(nullptr);
|
||||
xcb_unmap_window(ui_->connection(), wid_);
|
||||
if (ui_->pointerGrabber() == this) {
|
||||
ui_->ungrabPointer();
|
||||
}
|
||||
}
|
||||
|
||||
void XCBMenu::hideParents() {
|
||||
// FCITX_INFO() << "Hide Parent " << this;
|
||||
if (auto *parent = parent_.get()) {
|
||||
parent->hideParents();
|
||||
parent->hide();
|
||||
}
|
||||
}
|
||||
|
||||
void XCBMenu::hideChilds() {
|
||||
// FCITX_INFO() << "Hide Childs " << this;
|
||||
if (auto *child = child_.get()) {
|
||||
child->hideChilds();
|
||||
child->hide();
|
||||
}
|
||||
}
|
||||
|
||||
void XCBMenu::hideAll() {
|
||||
hideParents();
|
||||
hide();
|
||||
hideChilds();
|
||||
}
|
||||
|
||||
bool XCBMenu::childHasMouse() const {
|
||||
auto ref = child_;
|
||||
while (auto *child = ref.get()) {
|
||||
if (child->hasMouse_) {
|
||||
return true;
|
||||
}
|
||||
ref = child->child_;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
XCBMenu *XCBMenu::childByPosition(int rootX, int rootY) {
|
||||
if (ui_->pointerGrabber() != this) {
|
||||
return this;
|
||||
}
|
||||
|
||||
XCBMenu *result = this;
|
||||
while (auto *child = result->child_.get()) {
|
||||
result = child;
|
||||
}
|
||||
while (result) {
|
||||
Rect rect;
|
||||
rect.setPosition(result->x_, result->y_)
|
||||
.setSize(result->physicalWidth_, result->physicalHeight_);
|
||||
if (rect.contains(rootX, rootY)) {
|
||||
break;
|
||||
}
|
||||
result = result->parent_.get();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void XCBMenu::hideTillMenuHasMouseOrTopLevel() {
|
||||
// Go to the innermost child.
|
||||
auto *menu = this;
|
||||
while (auto *child = menu->child_.get()) {
|
||||
menu = child;
|
||||
}
|
||||
|
||||
menu->hideTillMenuHasMouseOrTopLevelHelper();
|
||||
}
|
||||
|
||||
void XCBMenu::hideTillMenuHasMouseOrTopLevelHelper() {
|
||||
if (parent_.isNull() || hasMouse_) {
|
||||
update();
|
||||
return;
|
||||
}
|
||||
auto *parent = parent_.get();
|
||||
hide(); // Hide will reset parent.
|
||||
if (parent) {
|
||||
parent->hideTillMenuHasMouseOrTopLevelHelper();
|
||||
}
|
||||
}
|
||||
|
||||
void XCBMenu::setHoveredIndex(int idx) {
|
||||
if (hoveredIndex_ == idx) {
|
||||
return;
|
||||
}
|
||||
// FCITX_INFO() << this << " setHoveredIndex(): " << idx
|
||||
// << " hasMouse: " << hasMouse_
|
||||
// << " child is valid: " << child_.isValid();
|
||||
|
||||
hoveredIndex_ = idx;
|
||||
update();
|
||||
pool_->setPopupMenuTimer(
|
||||
ui_->parent()->instance()->eventLoop().addTimeEvent(
|
||||
CLOCK_MONOTONIC, now(CLOCK_MONOTONIC) + 300000, 0,
|
||||
[this, that = this->watch()](EventSourceTime *, uint64_t) {
|
||||
if (!that.isValid()) {
|
||||
return true;
|
||||
}
|
||||
do {
|
||||
// FCITX_INFO() << this << " in timer";
|
||||
if (hoveredIndex_ >= 0 && subMenuIndex_ == hoveredIndex_) {
|
||||
// Mouse is on same menu item.
|
||||
break;
|
||||
}
|
||||
|
||||
if (hoveredIndex_ >= 0) {
|
||||
// FCITX_INFO() << this << " in timer branch 1";
|
||||
// The current subMenu anyway is not the hovered one.
|
||||
hideChilds();
|
||||
subMenuIndex_ = -1;
|
||||
auto item =
|
||||
actionAt(static_cast<size_t>(hoveredIndex_));
|
||||
if (!item.first || !item.second) {
|
||||
break;
|
||||
}
|
||||
// If we agree on this that current item has subMenu
|
||||
if (item.first->hasSubMenu_ && item.second->menu()) {
|
||||
auto *newMenu = pool_->requestMenu(
|
||||
ui_, item.second->menu(), this);
|
||||
subMenuIndex_ = hoveredIndex_;
|
||||
|
||||
// FCITX_INFO() << this << " in timer show submenu "
|
||||
// << newMenu;
|
||||
newMenu->show(
|
||||
physicalFromLogical(item.first->region_)
|
||||
.translated(x_, y_),
|
||||
ConstrainAdjustment::Slide);
|
||||
}
|
||||
} else {
|
||||
/// FCITX_INFO() << this << " in timer branch 2";
|
||||
// If we are not display any sub menu, and we don't have
|
||||
// mouse in the window.
|
||||
hideTillMenuHasMouseOrTopLevel();
|
||||
}
|
||||
update();
|
||||
} while (0);
|
||||
pool_->setPopupMenuTimer(nullptr);
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
||||
std::pair<MenuItem *, Action *> XCBMenu::actionAt(size_t index) {
|
||||
if (items_.size() <= index) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto actions = menu_->actions();
|
||||
if (actions.size() <= index || actions.size() != items_.size()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {&items_[index], actions[index]};
|
||||
}
|
||||
|
||||
void XCBMenu::updateDPI(int x, int y) {
|
||||
setScale(scaleForDPI(ui_->dpiByPosition(x, y)));
|
||||
}
|
||||
|
||||
void XCBMenu::updateBlur() {
|
||||
if (!atomBlur_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto width =
|
||||
static_cast<unsigned int>(YGNodeLayoutGetWidth(rootNode_.get()));
|
||||
const auto height =
|
||||
static_cast<unsigned int>(YGNodeLayoutGetHeight(rootNode_.get()));
|
||||
Rect logicalRect(0, 0, width, height);
|
||||
auto &theme = ui_->parent()->theme();
|
||||
const auto &menu = *theme.menu;
|
||||
shrink(logicalRect, *menu.blurMargin);
|
||||
if (!*menu.enableBlur || logicalRect.isEmpty()) {
|
||||
xcb_delete_property(ui_->connection(), wid_, atomBlur_);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto rect = physicalFromLogical(logicalRect);
|
||||
std::vector<uint32_t> data;
|
||||
if (menu.blurMask->empty()) {
|
||||
data = {static_cast<uint32_t>(rect.left()),
|
||||
static_cast<uint32_t>(rect.top()),
|
||||
static_cast<uint32_t>(rect.width()),
|
||||
static_cast<uint32_t>(rect.height())};
|
||||
} else {
|
||||
const auto region =
|
||||
theme.mask(theme.menuBlurMaskConfig(), width, height);
|
||||
for (const auto &maskRect : region) {
|
||||
const auto physicalRect = physicalFromLogical(maskRect);
|
||||
data.push_back(physicalRect.left());
|
||||
data.push_back(physicalRect.top());
|
||||
data.push_back(physicalRect.width());
|
||||
data.push_back(physicalRect.height());
|
||||
}
|
||||
}
|
||||
xcb_change_property(ui_->connection(), XCB_PROP_MODE_REPLACE, wid_,
|
||||
atomBlur_, XCB_ATOM_CARDINAL, 32, data.size(),
|
||||
data.data());
|
||||
}
|
||||
|
||||
void XCBMenu::renderYogaNode(cairo_t *cr, YGNodeRef node) {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
cairo_save(cr);
|
||||
cairo_translate(cr, YGNodeLayoutGetLeft(node), YGNodeLayoutGetTop(node));
|
||||
|
||||
cairoSetSourceColor(cr, node == rootNode_.get() ? Color(0, 0, 255, 128)
|
||||
: Color(255, 0, 0, 76));
|
||||
cairo_rectangle(cr, 0, 0, YGNodeLayoutGetWidth(node),
|
||||
YGNodeLayoutGetHeight(node));
|
||||
cairo_stroke(cr);
|
||||
|
||||
const auto childCount = YGNodeGetChildCount(node);
|
||||
for (uint32_t i = 0; i < childCount; i++) {
|
||||
renderYogaNode(cr, YGNodeGetChild(node, i));
|
||||
}
|
||||
|
||||
cairo_restore(cr);
|
||||
}
|
||||
|
||||
void XCBMenu::update() {
|
||||
auto *ic = lastRelevantIc();
|
||||
if (!ic) {
|
||||
ic = ui_->parent()
|
||||
->instance()
|
||||
->inputContextManager()
|
||||
.dummyInputContext();
|
||||
}
|
||||
|
||||
auto updateIfLarger = [](size_t &m, size_t n) { m = std::max(n, m); };
|
||||
|
||||
auto actions = menu_->actions();
|
||||
while (items_.size() < actions.size()) {
|
||||
items_.emplace_back(context_.get());
|
||||
}
|
||||
items_.erase(items_.begin() + actions.size(), items_.end());
|
||||
auto &theme = ui_->parent()->theme();
|
||||
auto *fontDesc = pango_font_description_from_string(
|
||||
ui_->parent()->config().menuFont->c_str());
|
||||
pango_context_set_font_description(context_.get(), fontDesc);
|
||||
pango_font_description_free(fontDesc);
|
||||
ui_->fontOption().setupPangoContext(context_.get());
|
||||
|
||||
const auto &textMargin = *theme.menu->textMargin;
|
||||
int i = 0;
|
||||
const auto &separator = theme.loadBackground(*theme.menu->separator);
|
||||
const auto &checkBox = theme.loadBackground(*theme.menu->checkBox);
|
||||
const auto &subMenu = theme.loadBackground(*theme.menu->subMenu);
|
||||
const auto &highlightMargin = *theme.menu->highlight->margin;
|
||||
size_t maxItemWidth = 0;
|
||||
size_t maxItemHeight = 0;
|
||||
|
||||
bool hasCheckable = std::ranges::any_of(actions, [](const Action *action) {
|
||||
return action->isCheckable() && !action->isSeparator();
|
||||
});
|
||||
for (auto *action : actions) {
|
||||
auto &item = items_[i];
|
||||
item.isHighlight_ =
|
||||
hoveredIndex_ >= 0 ? (hoveredIndex_ == i) : (subMenuIndex_ == i);
|
||||
i++;
|
||||
item.hasSubMenu_ = action->menu() != nullptr;
|
||||
item.isSeparator_ = action->isSeparator();
|
||||
if (action->isSeparator()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate size for real items.
|
||||
auto text = action->shortText(ic);
|
||||
pango_layout_context_changed(item.layout_.get());
|
||||
pango_layout_set_text(item.layout_.get(), text.c_str(), text.size());
|
||||
item.textWidth_ = item.textHeight_ = 0;
|
||||
pango_layout_get_pixel_size(item.layout_.get(), &item.textWidth_,
|
||||
&item.textHeight_);
|
||||
|
||||
size_t itemWidth = 0;
|
||||
size_t itemHeight = 0;
|
||||
if (hasCheckable) {
|
||||
itemWidth += checkBox.width();
|
||||
updateIfLarger(itemHeight, checkBox.height());
|
||||
}
|
||||
item.isChecked_ = action->isChecked(ic);
|
||||
itemWidth += item.textWidth_;
|
||||
updateIfLarger(itemHeight, item.textHeight_);
|
||||
itemWidth += subMenu.width();
|
||||
updateIfLarger(itemHeight, subMenu.height());
|
||||
|
||||
updateIfLarger(maxItemWidth, itemWidth);
|
||||
updateIfLarger(maxItemHeight, itemHeight);
|
||||
}
|
||||
|
||||
YGNodeRemoveAllChildren(rootNode_.get());
|
||||
YGNodeReset(rootNode_.get());
|
||||
YGNodeStyleSetFlexDirection(rootNode_.get(), YGFlexDirectionColumn);
|
||||
YGNodeStyleSetPadding(rootNode_.get(), YGEdgeLeft,
|
||||
*theme.menu->contentMargin->marginLeft);
|
||||
YGNodeStyleSetPadding(rootNode_.get(), YGEdgeRight,
|
||||
*theme.menu->contentMargin->marginRight);
|
||||
YGNodeStyleSetPadding(rootNode_.get(), YGEdgeTop,
|
||||
*theme.menu->contentMargin->marginTop);
|
||||
YGNodeStyleSetPadding(rootNode_.get(), YGEdgeBottom,
|
||||
*theme.menu->contentMargin->marginBottom);
|
||||
YGNodeStyleSetMinWidth(rootNode_.get(), 1);
|
||||
YGNodeStyleSetMinHeight(rootNode_.get(), 1);
|
||||
YGNodeStyleSetGap(rootNode_.get(), YGGutterRow, *theme.menu->spacing);
|
||||
|
||||
for (size_t index = 0; index < items_.size(); index++) {
|
||||
auto &item = items_[index];
|
||||
YGNodeRemoveAllChildren(item.self_.get());
|
||||
YGNodeRemoveAllChildren(item.leading_.get());
|
||||
YGNodeReset(item.self_.get());
|
||||
YGNodeReset(item.leading_.get());
|
||||
YGNodeReset(item.checkBox_.get());
|
||||
YGNodeReset(item.text_.get());
|
||||
YGNodeReset(item.subMenu_.get());
|
||||
|
||||
if (item.isSeparator_) {
|
||||
YGNodeStyleSetHeight(item.self_.get(), separator.isPattern()
|
||||
? 2
|
||||
: separator.height());
|
||||
YGNodeInsertChild(rootNode_.get(), item.self_.get(), index);
|
||||
continue;
|
||||
}
|
||||
|
||||
YGNodeStyleSetFlexDirection(item.self_.get(), YGFlexDirectionRow);
|
||||
YGNodeStyleSetJustifyContent(item.self_.get(), YGJustifySpaceBetween);
|
||||
YGNodeStyleSetAlignItems(item.self_.get(), YGAlignCenter);
|
||||
YGNodeStyleSetWidth(item.self_.get(), maxItemWidth);
|
||||
YGNodeStyleSetHeight(item.self_.get(), maxItemHeight);
|
||||
YGNodeStyleSetMargin(item.self_.get(), YGEdgeLeft,
|
||||
*textMargin.marginLeft);
|
||||
YGNodeStyleSetMargin(item.self_.get(), YGEdgeRight,
|
||||
*textMargin.marginRight);
|
||||
YGNodeStyleSetMargin(item.self_.get(), YGEdgeTop,
|
||||
*textMargin.marginTop);
|
||||
YGNodeStyleSetMargin(item.self_.get(), YGEdgeBottom,
|
||||
*textMargin.marginBottom);
|
||||
|
||||
YGNodeStyleSetFlexDirection(item.leading_.get(), YGFlexDirectionRow);
|
||||
YGNodeStyleSetAlignItems(item.leading_.get(), YGAlignCenter);
|
||||
if (hasCheckable) {
|
||||
YGNodeStyleSetWidth(item.checkBox_.get(), checkBox.width());
|
||||
YGNodeStyleSetHeight(item.checkBox_.get(), checkBox.height());
|
||||
YGNodeInsertChild(item.leading_.get(), item.checkBox_.get(), 0);
|
||||
}
|
||||
YGNodeStyleSetWidth(item.text_.get(), item.textWidth_);
|
||||
YGNodeStyleSetHeight(item.text_.get(), item.textHeight_);
|
||||
YGNodeStyleSetWidth(item.subMenu_.get(), subMenu.width());
|
||||
YGNodeStyleSetHeight(item.subMenu_.get(), subMenu.height());
|
||||
YGNodeInsertChild(item.leading_.get(), item.text_.get(), hasCheckable);
|
||||
YGNodeInsertChild(item.self_.get(), item.leading_.get(), 0);
|
||||
YGNodeInsertChild(item.self_.get(), item.subMenu_.get(), 1);
|
||||
YGNodeInsertChild(rootNode_.get(), item.self_.get(), index);
|
||||
}
|
||||
|
||||
YGNodeCalculateLayout(rootNode_.get(), YGUndefined, YGUndefined,
|
||||
YGDirectionLTR);
|
||||
const auto width =
|
||||
static_cast<unsigned int>(YGNodeLayoutGetWidth(rootNode_.get()));
|
||||
const auto height =
|
||||
static_cast<unsigned int>(YGNodeLayoutGetHeight(rootNode_.get()));
|
||||
|
||||
const auto oldPhysicalWidth = physicalWidth_;
|
||||
const auto oldPhysicalHeight = physicalHeight_;
|
||||
resize(width, height);
|
||||
if (physicalWidth_ != oldPhysicalWidth ||
|
||||
physicalHeight_ != oldPhysicalHeight) {
|
||||
updateBlur();
|
||||
}
|
||||
|
||||
cairo_t *c = cairo_create(prerender());
|
||||
auto surface = cairo_get_target(c);
|
||||
auto surfaceWidth = cairo_image_surface_get_width(surface);
|
||||
auto surfaceHeight = cairo_image_surface_get_height(surface);
|
||||
auto renderWidth = surfaceWidth / scale_;
|
||||
auto renderHeight = surfaceHeight / scale_;
|
||||
cairo_set_operator(c, CAIRO_OPERATOR_CLEAR);
|
||||
cairo_paint(c);
|
||||
cairo_set_operator(c, CAIRO_OPERATOR_OVER);
|
||||
theme.paint(c, *theme.menu->background, 0, 0, renderWidth, renderHeight,
|
||||
/*alpha=*/1.0);
|
||||
for (auto &item : items_) {
|
||||
if (item.isSeparator_) {
|
||||
const ThemeImage &separator =
|
||||
theme.loadBackground(*theme.menu->separator);
|
||||
theme.paint(c, *theme.menu->separator,
|
||||
absolute<YGNodeLayoutGetLeft>(item.self_.get()),
|
||||
absolute<YGNodeLayoutGetTop>(item.self_.get()),
|
||||
renderWidth - *theme.menu->contentMargin->marginLeft -
|
||||
*theme.menu->contentMargin->marginRight,
|
||||
(separator.isPattern() ? 2 : -1), /*alpha=*/1.0);
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto itemLeft = absolute<YGNodeLayoutGetLeft>(item.self_.get());
|
||||
const auto itemTop = absolute<YGNodeLayoutGetTop>(item.self_.get());
|
||||
const auto itemWidth = YGNodeLayoutGetWidth(item.self_.get());
|
||||
const auto itemHeight = YGNodeLayoutGetHeight(item.self_.get());
|
||||
item.region_
|
||||
.setPosition(itemLeft - *highlightMargin.marginLeft,
|
||||
itemTop - *highlightMargin.marginTop)
|
||||
.setSize(itemWidth + *highlightMargin.marginLeft +
|
||||
*highlightMargin.marginRight,
|
||||
itemHeight + *highlightMargin.marginTop +
|
||||
*highlightMargin.marginBottom);
|
||||
if (item.isHighlight_) {
|
||||
theme.paint(c, *theme.menu->highlight, item.region_.left(),
|
||||
item.region_.top(), item.region_.width(),
|
||||
item.region_.height(),
|
||||
/*alpha=*/1.0);
|
||||
}
|
||||
|
||||
if (item.isChecked_) {
|
||||
theme.paint(c, *theme.menu->checkBox,
|
||||
absolute<YGNodeLayoutGetLeft>(item.checkBox_.get()),
|
||||
absolute<YGNodeLayoutGetTop>(item.checkBox_.get()), -1,
|
||||
-1,
|
||||
/*alpha=*/1.0);
|
||||
}
|
||||
|
||||
if (item.hasSubMenu_) {
|
||||
theme.paint(c, *theme.menu->subMenu,
|
||||
absolute<YGNodeLayoutGetLeft>(item.subMenu_.get()),
|
||||
absolute<YGNodeLayoutGetTop>(item.subMenu_.get()), -1,
|
||||
-1,
|
||||
/*alpha=*/1.0);
|
||||
}
|
||||
|
||||
cairo_save(c);
|
||||
if (item.isHighlight_) {
|
||||
cairoSetSourceColor(c, theme.menuSelectedItemText());
|
||||
} else {
|
||||
cairoSetSourceColor(c, theme.menuText());
|
||||
}
|
||||
cairo_translate(c, absolute<YGNodeLayoutGetLeft>(item.text_.get()),
|
||||
absolute<YGNodeLayoutGetTop>(item.text_.get()));
|
||||
pango_cairo_show_layout(c, item.layout_.get());
|
||||
cairo_restore(c);
|
||||
}
|
||||
|
||||
if (m3ui_logcategory().checkLogLevel(Debug)) {
|
||||
renderYogaNode(c, rootNode_.get());
|
||||
}
|
||||
|
||||
cairo_destroy(c);
|
||||
render();
|
||||
}
|
||||
|
||||
void XCBMenu::postCreateWindow() {
|
||||
if (ui_->ewmh()->_NET_WM_WINDOW_TYPE_MENU &&
|
||||
ui_->ewmh()->_NET_WM_WINDOW_TYPE_POPUP_MENU &&
|
||||
ui_->ewmh()->_NET_WM_WINDOW_TYPE) {
|
||||
uint32_t types[] = {ui_->ewmh()->_NET_WM_WINDOW_TYPE_MENU,
|
||||
ui_->ewmh()->_NET_WM_WINDOW_TYPE_POPUP_MENU};
|
||||
xcb_ewmh_set_wm_window_type(ui_->ewmh(), wid_, 2, types);
|
||||
}
|
||||
|
||||
if (ui_->ewmh()->_NET_WM_PID) {
|
||||
xcb_ewmh_set_wm_pid(ui_->ewmh(), wid_, getpid());
|
||||
}
|
||||
|
||||
const char name[] = "Fcitx5 Menu Window";
|
||||
xcb_icccm_set_wm_name(ui_->connection(), wid_, XCB_ATOM_STRING, 8,
|
||||
sizeof(name) - 1, name);
|
||||
const char klass[] = "fcitx\0fcitx";
|
||||
xcb_icccm_set_wm_class(ui_->connection(), wid_, sizeof(klass) - 1, klass);
|
||||
addEventMaskToWindow(
|
||||
ui_->connection(), wid_,
|
||||
XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_BUTTON_PRESS |
|
||||
XCB_EVENT_MASK_BUTTON_RELEASE | XCB_EVENT_MASK_FOCUS_CHANGE |
|
||||
XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW |
|
||||
XCB_EVENT_MASK_VISIBILITY_CHANGE | XCB_EVENT_MASK_POINTER_MOTION);
|
||||
}
|
||||
|
||||
void XCBMenu::setParent(XCBMenu *parent) {
|
||||
if (auto *oldParent = parent_.get()) {
|
||||
if (parent == oldParent) {
|
||||
return;
|
||||
}
|
||||
|
||||
parent_.unwatch();
|
||||
oldParent->setChild(nullptr);
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
parent_ = parent->watch();
|
||||
parent->setChild(this);
|
||||
} else {
|
||||
parent_.unwatch();
|
||||
}
|
||||
}
|
||||
|
||||
void XCBMenu::setChild(XCBMenu *child) {
|
||||
if (child) {
|
||||
child_ = child->watch();
|
||||
} else {
|
||||
child_.unwatch();
|
||||
subMenuIndex_ = -1;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void XCBMenu::setInputContext(TrackableObjectReference<InputContext> ic) {
|
||||
lastRelevantIc_ = std::move(ic);
|
||||
}
|
||||
|
||||
InputContext *XCBMenu::lastRelevantIc() {
|
||||
if (auto *ic = lastRelevantIc_.get()) {
|
||||
return ic;
|
||||
}
|
||||
return ui_->parent()->instance()->mostRecentInputContext();
|
||||
}
|
||||
|
||||
void XCBMenu::show(Rect rect, ConstrainAdjustment adjustY) {
|
||||
// FCITX_INFO() << this << " show() " << hoveredIndex_;
|
||||
if (visible_) {
|
||||
return;
|
||||
}
|
||||
visible_ = true;
|
||||
hoveredIndex_ = -1;
|
||||
subMenuIndex_ = -1;
|
||||
int x = rect.left();
|
||||
int y = rect.top();
|
||||
updateDPI(x, y);
|
||||
update();
|
||||
updateBlur();
|
||||
const Rect *closestScreen = nullptr;
|
||||
int shortestDistance = INT_MAX;
|
||||
for (const auto &rect : ui_->screenRects()) {
|
||||
int thisDistance = rect.first.distance(x, y);
|
||||
if (thisDistance < shortestDistance) {
|
||||
shortestDistance = thisDistance;
|
||||
closestScreen = &rect.first;
|
||||
}
|
||||
}
|
||||
|
||||
x = x + rect.width();
|
||||
|
||||
if (closestScreen) {
|
||||
if (x + physicalWidth_ > closestScreen->right()) {
|
||||
x = rect.left() - physicalWidth_;
|
||||
}
|
||||
|
||||
switch (adjustY) {
|
||||
case ConstrainAdjustment::Slide:
|
||||
if (y + physicalHeight_ > closestScreen->bottom()) {
|
||||
y = closestScreen->bottom() - physicalHeight_;
|
||||
}
|
||||
break;
|
||||
case ConstrainAdjustment::Flip:
|
||||
if (y + physicalHeight_ > closestScreen->bottom()) {
|
||||
y = rect.top() - physicalHeight_;
|
||||
}
|
||||
break;
|
||||
};
|
||||
|
||||
y = std::max(y, closestScreen->top());
|
||||
}
|
||||
|
||||
xcb_params_configure_window_t wc;
|
||||
wc.x = x;
|
||||
wc.y = y;
|
||||
wc.stack_mode = XCB_STACK_MODE_ABOVE;
|
||||
xcb_aux_configure_window(ui_->connection(), wid_,
|
||||
XCB_CONFIG_WINDOW_STACK_MODE |
|
||||
XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y,
|
||||
&wc);
|
||||
|
||||
xcb_map_window(ui_->connection(), wid_);
|
||||
if (parent_.isNull()) {
|
||||
ui_->grabPointer(this);
|
||||
}
|
||||
x_ = x;
|
||||
y_ = y;
|
||||
}
|
||||
|
||||
void XCBMenu::raise() {
|
||||
xcb_params_configure_window_t wc;
|
||||
wc.stack_mode = XCB_STACK_MODE_ABOVE;
|
||||
xcb_aux_configure_window(ui_->connection(), wid_,
|
||||
XCB_CONFIG_WINDOW_STACK_MODE, &wc);
|
||||
}
|
||||
|
||||
XCBMenu *MenuPool::requestMenu(XCBUI *ui, Menu *menu, XCBMenu *parent) {
|
||||
auto *xcbMenu = findOrCreateMenu(ui, menu);
|
||||
xcbMenu->setParent(parent);
|
||||
if (parent) {
|
||||
xcbMenu->setInputContext(parent->inputContext());
|
||||
} else {
|
||||
if (auto *ic = ui->parent()->instance()->mostRecentInputContext()) {
|
||||
xcbMenu->setInputContext(ic->watch());
|
||||
} else {
|
||||
xcbMenu->setInputContext({});
|
||||
}
|
||||
}
|
||||
return xcbMenu;
|
||||
}
|
||||
|
||||
XCBMenu *MenuPool::findOrCreateMenu(XCBUI *ui, Menu *menu) {
|
||||
auto iter = pool_.find(menu);
|
||||
if (iter != pool_.end()) {
|
||||
return &iter->second.first;
|
||||
}
|
||||
|
||||
ScopedConnection conn = menu->connect<ObjectDestroyed>([this](void *data) {
|
||||
Menu *menu = static_cast<Menu *>(data);
|
||||
pool_.erase(menu);
|
||||
});
|
||||
|
||||
auto result = pool_.emplace(
|
||||
std::piecewise_construct, std::forward_as_tuple(menu),
|
||||
std::forward_as_tuple(std::piecewise_construct,
|
||||
std::forward_as_tuple(ui, this, menu),
|
||||
std::forward_as_tuple(std::move(conn))));
|
||||
return &result.first->second.first;
|
||||
}
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2017-2017 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#ifndef _FCITX_UI_CLASSIC_XCBMENU_H_
|
||||
#define _FCITX_UI_CLASSIC_XCBMENU_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <pango/pango-layout.h>
|
||||
#include <pango/pango-types.h>
|
||||
#include <xcb/xcb.h>
|
||||
#include <yoga/YGConfig.h>
|
||||
#include <yoga/YGNode.h>
|
||||
#include "fcitx-utils/eventloopinterface.h"
|
||||
#include "fcitx-utils/misc.h"
|
||||
#include "fcitx-utils/rect.h"
|
||||
#include "fcitx-utils/signals.h"
|
||||
#include "fcitx-utils/trackableobject.h"
|
||||
#include "fcitx/action.h"
|
||||
#include "fcitx/inputcontext.h"
|
||||
#include "fcitx/menu.h"
|
||||
#include "common.h"
|
||||
#include "xcbui.h"
|
||||
#include "xcbwindow.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
class MenuPool;
|
||||
|
||||
struct MenuItem {
|
||||
MenuItem(PangoContext *context) : layout_(pango_layout_new(context)) {
|
||||
self_.reset(YGNodeNew());
|
||||
leading_.reset(YGNodeNew());
|
||||
checkBox_.reset(YGNodeNew());
|
||||
text_.reset(YGNodeNew());
|
||||
subMenu_.reset(YGNodeNew());
|
||||
}
|
||||
|
||||
bool hasSubMenu_ = false;
|
||||
bool isHighlight_ = false;
|
||||
bool isSeparator_ = false;
|
||||
bool isChecked_ = false;
|
||||
GObjectUniquePtr<PangoLayout> layout_;
|
||||
Rect region_;
|
||||
int textWidth_ = 0, textHeight_ = 0;
|
||||
UniqueCPtr<YGNode, YGNodeFree> self_;
|
||||
UniqueCPtr<YGNode, YGNodeFree> leading_;
|
||||
UniqueCPtr<YGNode, YGNodeFree> checkBox_;
|
||||
UniqueCPtr<YGNode, YGNodeFree> text_;
|
||||
UniqueCPtr<YGNode, YGNodeFree> subMenu_;
|
||||
};
|
||||
|
||||
enum class ConstrainAdjustment { Slide, Flip };
|
||||
|
||||
class XCBMenu : public XCBWindow, public TrackableObject<XCBMenu> {
|
||||
public:
|
||||
XCBMenu(XCBUI *ui, MenuPool *pool, Menu *menu);
|
||||
~XCBMenu();
|
||||
void show(Rect rect, ConstrainAdjustment adjustY);
|
||||
|
||||
// Hide menu itself.
|
||||
void hide();
|
||||
|
||||
// Hide all of its parent.
|
||||
void hideParents();
|
||||
|
||||
// Hide all menu on the chain until the one has mouse.
|
||||
void hideTillMenuHasMouseOrTopLevel();
|
||||
|
||||
// Hide all of its child.
|
||||
void hideChilds();
|
||||
|
||||
void hideAll();
|
||||
|
||||
// Raise the menu.
|
||||
void raise();
|
||||
|
||||
bool filterEvent(xcb_generic_event_t *event) override;
|
||||
void postCreateWindow() override;
|
||||
|
||||
void setParent(XCBMenu *parent);
|
||||
void setInputContext(TrackableObjectReference<InputContext> ic);
|
||||
TrackableObjectReference<InputContext> inputContext() const {
|
||||
return lastRelevantIc_;
|
||||
}
|
||||
|
||||
bool childHasMouse() const;
|
||||
|
||||
private:
|
||||
void handleButtonPress(int eventX, int eventY);
|
||||
void handleMotionNotify(int eventX, int eventY);
|
||||
XCBMenu *childByPosition(int rootX, int rootY);
|
||||
|
||||
void hideTillMenuHasMouseOrTopLevelHelper();
|
||||
InputContext *lastRelevantIc();
|
||||
void update();
|
||||
void setHoveredIndex(int idx);
|
||||
void setChild(XCBMenu *child);
|
||||
void updateDPI(int x, int y);
|
||||
void updateBlur();
|
||||
std::pair<MenuItem *, Action *> actionAt(size_t index);
|
||||
void renderYogaNode(cairo_t *cr, YGNodeRef node);
|
||||
|
||||
MenuPool *pool_;
|
||||
|
||||
GObjectUniquePtr<PangoFontMap> fontMap_;
|
||||
GObjectUniquePtr<PangoContext> context_;
|
||||
std::vector<MenuItem> items_;
|
||||
UniqueCPtr<YGNode, YGNodeFree> rootNode_;
|
||||
|
||||
ScopedConnection destroyed_;
|
||||
TrackableObjectReference<InputContext> lastRelevantIc_;
|
||||
Menu *menu_;
|
||||
TrackableObjectReference<XCBMenu> parent_;
|
||||
TrackableObjectReference<XCBMenu> child_;
|
||||
int x_ = 0;
|
||||
int y_ = 0;
|
||||
bool hasMouse_ = false;
|
||||
bool visible_ = false;
|
||||
int subMenuIndex_ = -1;
|
||||
int hoveredIndex_ = -1;
|
||||
xcb_atom_t atomBlur_;
|
||||
std::unique_ptr<EventSourceTime> activateTimer_;
|
||||
};
|
||||
|
||||
class MenuPool {
|
||||
public:
|
||||
XCBMenu *requestMenu(XCBUI *ui, Menu *menu, XCBMenu *parent);
|
||||
|
||||
void setPopupMenuTimer(std::unique_ptr<EventSourceTime> popupMenuTimer) {
|
||||
popupMenuTimer_ = std::move(popupMenuTimer);
|
||||
}
|
||||
|
||||
private:
|
||||
XCBMenu *findOrCreateMenu(XCBUI *ui, Menu *menu);
|
||||
|
||||
std::unordered_map<Menu *, std::pair<XCBMenu, ScopedConnection>> pool_;
|
||||
std::unique_ptr<EventSourceTime> popupMenuTimer_;
|
||||
};
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
|
||||
#endif // _FCITX_UI_CLASSIC_XCBMENU_H_
|
||||
@@ -0,0 +1,543 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2017-2017 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#include "xcbtraywindow.h"
|
||||
#include <unistd.h>
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cairo.h>
|
||||
#include <xcb/xcb.h>
|
||||
#include <xcb/xcb_aux.h>
|
||||
#include <xcb/xcb_ewmh.h>
|
||||
#include <xcb/xcb_icccm.h>
|
||||
#include <xcb/xproto.h>
|
||||
#include "fcitx-utils/i18n.h"
|
||||
#include "fcitx-utils/misc.h"
|
||||
#include "fcitx-utils/rect.h"
|
||||
#include "fcitx/action.h"
|
||||
#include "fcitx/inputcontext.h"
|
||||
#include "fcitx/inputmethodentry.h"
|
||||
#include "fcitx/inputmethodmanager.h"
|
||||
#include "fcitx/statusarea.h"
|
||||
#include "fcitx/userinterfacemanager.h"
|
||||
#include "common.h"
|
||||
#include "xcb_public.h"
|
||||
#include "xcbmenu.h"
|
||||
#include "xcbui.h"
|
||||
#include "xcbwindow.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
#define SYSTEM_TRAY_REQUEST_DOCK 0
|
||||
#define SYSTEM_TRAY_BEGIN_MESSAGE 1
|
||||
#define SYSTEM_TRAY_CANCEL_MESSAGE 2
|
||||
|
||||
#define ATOM_SELECTION 0
|
||||
#define ATOM_MANAGER 1
|
||||
#define ATOM_SYSTEM_TRAY_OPCODE 2
|
||||
#define ATOM_ORIENTATION 3
|
||||
#define ATOM_VISUAL 4
|
||||
|
||||
XCBTrayWindow::XCBTrayWindow(XCBUI *ui) : XCBWindow(ui, 48, 48) {
|
||||
for (auto &separator : separatorActions_) {
|
||||
separator.setSeparator(true);
|
||||
}
|
||||
groupAction_.setShortText(_("Group"));
|
||||
groupAction_.setMenu(&groupMenu_);
|
||||
configureAction_.setShortText(_("Input Method Settings"));
|
||||
restartAction_.setShortText(_("Restart"));
|
||||
exitAction_.setShortText(_("Exit"));
|
||||
menu_.addAction(&groupAction_);
|
||||
menu_.addAction(&separatorActions_[0]);
|
||||
menu_.addAction(&configureAction_);
|
||||
menu_.addAction(&restartAction_);
|
||||
menu_.addAction(&exitAction_);
|
||||
|
||||
configureAction_.connect<SimpleAction::Activated>(
|
||||
[this](InputContext *) { ui_->parent()->instance()->configure(); });
|
||||
restartAction_.connect<SimpleAction::Activated>(
|
||||
[this](InputContext *) { ui_->parent()->instance()->restart(); });
|
||||
exitAction_.connect<SimpleAction::Activated>(
|
||||
[this](InputContext *) { ui_->parent()->instance()->exit(); });
|
||||
|
||||
auto &uiManager = ui_->parent()->instance()->userInterfaceManager();
|
||||
uiManager.registerAction(&groupAction_);
|
||||
uiManager.registerAction(&configureAction_);
|
||||
uiManager.registerAction(&restartAction_);
|
||||
uiManager.registerAction(&exitAction_);
|
||||
|
||||
#if 0
|
||||
inputMethodMenu_.addAction(&testAction1_);
|
||||
inputMethodMenu_.addAction(&testAction2_);
|
||||
testAction1_.setMenu(&testMenu1_);
|
||||
testAction2_.setMenu(&testMenu2_);
|
||||
testMenu1_.addAction(&testSubAction1_);
|
||||
testMenu2_.addAction(&testSubAction2_);
|
||||
testAction1_.setShortText("TEST1");
|
||||
testAction2_.setShortText("TEST2");
|
||||
testSubAction1_.setShortText("TESTSUB1");
|
||||
testSubAction2_.setShortText("TESTSUB2");
|
||||
uiManager.registerAction(&testAction1_);
|
||||
uiManager.registerAction(&testAction2_);
|
||||
uiManager.registerAction(&testSubAction1_);
|
||||
uiManager.registerAction(&testSubAction2_);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool XCBTrayWindow::filterEvent(xcb_generic_event_t *event) {
|
||||
uint8_t response_type = event->response_type & ~0x80;
|
||||
switch (response_type) {
|
||||
case XCB_CLIENT_MESSAGE: {
|
||||
auto *client_message =
|
||||
reinterpret_cast<xcb_client_message_event_t *>(event);
|
||||
if (client_message->type == atoms_[ATOM_MANAGER] &&
|
||||
client_message->format == 32 &&
|
||||
client_message->data.data32[1] == atoms_[ATOM_SELECTION] &&
|
||||
dockWindow_ == XCB_WINDOW_NONE) {
|
||||
refreshDockWindow();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case XCB_BUTTON_PRESS: {
|
||||
auto *press = reinterpret_cast<xcb_button_press_event_t *>(event);
|
||||
if (press->event == wid_) {
|
||||
if (press->detail == XCB_BUTTON_INDEX_3) {
|
||||
updateMenu();
|
||||
XCBMenu *menu = menuPool_.requestMenu(ui_, &menu_, nullptr);
|
||||
menu->show(Rect()
|
||||
.setPosition(press->root_x, press->root_y)
|
||||
.setSize(1, 1),
|
||||
ConstrainAdjustment::Flip);
|
||||
} else if (press->detail == XCB_BUTTON_INDEX_1) {
|
||||
ui_->parent()->instance()->toggle();
|
||||
} else if (press->detail == XCB_BUTTON_INDEX_4) {
|
||||
ui_->parent()->instance()->enumerate(true);
|
||||
} else if (press->detail == XCB_BUTTON_INDEX_5) {
|
||||
ui_->parent()->instance()->enumerate(false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case XCB_EXPOSE: {
|
||||
auto *expose = reinterpret_cast<xcb_expose_event_t *>(event);
|
||||
if (expose->window == wid_) {
|
||||
CLASSICUI_DEBUG() << "Tray received expose event";
|
||||
update();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case XCB_CONFIGURE_NOTIFY: {
|
||||
auto *configure =
|
||||
reinterpret_cast<xcb_configure_notify_event_t *>(event);
|
||||
if (wid_ == configure->event) {
|
||||
CLASSICUI_DEBUG() << "Tray received configure event";
|
||||
hintHeight_ = configure->height;
|
||||
hintWidth_ = configure->width;
|
||||
resizeTrayWindow();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case XCB_DESTROY_NOTIFY: {
|
||||
auto *destroywindow =
|
||||
reinterpret_cast<xcb_destroy_notify_event_t *>(event);
|
||||
if (destroywindow->event == dockWindow_) {
|
||||
refreshDockWindow();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case XCB_PROPERTY_NOTIFY: {
|
||||
auto *property = reinterpret_cast<xcb_property_notify_event_t *>(event);
|
||||
if (property->atom == atoms_[ATOM_VISUAL] &&
|
||||
property->window == dockWindow_) {
|
||||
createTrayWindow();
|
||||
findDock();
|
||||
return true;
|
||||
}
|
||||
if (property->atom == atoms_[ATOM_ORIENTATION] &&
|
||||
property->window == dockWindow_) {
|
||||
isHorizontal_ = trayOrientation();
|
||||
resizeTrayWindow();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void XCBTrayWindow::initTray() {
|
||||
char trayAtomNameBuf[100];
|
||||
const char *atom_names[] = {
|
||||
trayAtomNameBuf, "MANAGER", "_NET_SYSTEM_TRAY_OPCODE",
|
||||
"_NET_SYSTEM_TRAY_ORIENTATION", "_NET_SYSTEM_TRAY_VISUAL"};
|
||||
|
||||
sprintf(trayAtomNameBuf, "_NET_SYSTEM_TRAY_S%d", ui_->defaultScreen());
|
||||
size_t i = 0;
|
||||
for (const auto *name : atom_names) {
|
||||
atoms_[i] = ui_->parent()->xcb()->call<IXCBModule::atom>(
|
||||
ui_->displayName(), name, false);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
void XCBTrayWindow::refreshDockWindow() {
|
||||
auto cookie = xcb_get_selection_owner(ui_->connection(), atoms_[0]);
|
||||
auto reply = makeUniqueCPtr(
|
||||
xcb_get_selection_owner_reply(ui_->connection(), cookie, nullptr));
|
||||
if (reply) {
|
||||
dockWindow_ = reply->owner;
|
||||
}
|
||||
|
||||
if (dockWindow_) {
|
||||
CLASSICUI_DEBUG() << "Found dock window";
|
||||
addEventMaskToWindow(ui_->connection(), dockWindow_,
|
||||
XCB_EVENT_MASK_STRUCTURE_NOTIFY);
|
||||
createTrayWindow();
|
||||
findDock();
|
||||
} else {
|
||||
destroyWindow();
|
||||
}
|
||||
}
|
||||
|
||||
void XCBTrayWindow::findDock() {
|
||||
if (!wid_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dockWindow_) {
|
||||
CLASSICUI_DEBUG() << "Send op code to tray";
|
||||
sendTrayOpcode(SYSTEM_TRAY_REQUEST_DOCK, wid_, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void XCBTrayWindow::createTrayWindow() {
|
||||
trayVid_ = trayVisual();
|
||||
isHorizontal_ = trayOrientation();
|
||||
if (trayVid_) {
|
||||
xcb_screen_t *screen =
|
||||
xcb_aux_get_screen(ui_->connection(), ui_->defaultScreen());
|
||||
trayDepth_ = xcb_aux_get_depth_of_visual(screen, trayVid_);
|
||||
} else {
|
||||
trayDepth_ = 0;
|
||||
}
|
||||
|
||||
createWindow(trayVid_);
|
||||
}
|
||||
|
||||
void XCBTrayWindow::resizeTrayWindow() {
|
||||
auto size = isHorizontal_ ? hintHeight_ : hintWidth_;
|
||||
if (width() != size && height() != size) {
|
||||
resize(size, size);
|
||||
xcb_size_hints_t size_hints;
|
||||
memset(&size_hints, 0, sizeof(size_hints));
|
||||
size_hints.flags = XCB_ICCCM_SIZE_HINT_BASE_SIZE;
|
||||
size_hints.base_width = size;
|
||||
size_hints.base_height = size;
|
||||
xcb_icccm_set_wm_normal_hints(ui_->connection(), wid_, &size_hints);
|
||||
}
|
||||
}
|
||||
|
||||
void XCBTrayWindow::sendTrayOpcode(long message, long data1, long data2,
|
||||
long data3) {
|
||||
xcb_client_message_event_t ev;
|
||||
|
||||
memset(&ev, 0, sizeof(ev));
|
||||
ev.response_type = XCB_CLIENT_MESSAGE;
|
||||
ev.window = dockWindow_;
|
||||
ev.type = atoms_[ATOM_SYSTEM_TRAY_OPCODE];
|
||||
ev.format = 32;
|
||||
ev.data.data32[0] = XCB_CURRENT_TIME;
|
||||
ev.data.data32[1] = message;
|
||||
ev.data.data32[2] = data1;
|
||||
ev.data.data32[3] = data2;
|
||||
ev.data.data32[4] = data3;
|
||||
|
||||
xcb_send_event(ui_->connection(), false, dockWindow_,
|
||||
XCB_EVENT_MASK_NO_EVENT, reinterpret_cast<char *>(&ev));
|
||||
}
|
||||
|
||||
xcb_visualid_t XCBTrayWindow::trayVisual() {
|
||||
xcb_visualid_t vid = 0;
|
||||
if (dockWindow_ == XCB_WINDOW_NONE) {
|
||||
return 0;
|
||||
}
|
||||
auto cookie =
|
||||
xcb_get_property(ui_->connection(), false, dockWindow_,
|
||||
atoms_[ATOM_VISUAL], XCB_ATOM_VISUALID, 0, 1);
|
||||
auto reply = makeUniqueCPtr(
|
||||
xcb_get_property_reply(ui_->connection(), cookie, nullptr));
|
||||
if (!reply || reply->type != XCB_ATOM_VISUALID || reply->format != 32 ||
|
||||
reply->bytes_after != 0) {
|
||||
return 0;
|
||||
}
|
||||
auto *data = static_cast<char *>(xcb_get_property_value(reply.get()));
|
||||
int length = xcb_get_property_value_length(reply.get());
|
||||
if (length != 32 / 8) {
|
||||
return 0;
|
||||
}
|
||||
vid = *reinterpret_cast<xcb_visualid_t *>(data);
|
||||
return vid;
|
||||
}
|
||||
|
||||
bool XCBTrayWindow::trayOrientation() {
|
||||
if (dockWindow_ == XCB_WINDOW_NONE) {
|
||||
return true;
|
||||
}
|
||||
auto cookie =
|
||||
xcb_get_property(ui_->connection(), false, dockWindow_,
|
||||
atoms_[ATOM_ORIENTATION], XCB_ATOM_CARDINAL, 0, 1);
|
||||
auto reply = makeUniqueCPtr(
|
||||
xcb_get_property_reply(ui_->connection(), cookie, nullptr));
|
||||
if (!reply || reply->type != XCB_ATOM_CARDINAL || reply->format != 32 ||
|
||||
reply->bytes_after != 0) {
|
||||
return true;
|
||||
}
|
||||
auto *data = static_cast<char *>(xcb_get_property_value(reply.get()));
|
||||
int length = xcb_get_property_value_length(reply.get());
|
||||
if (length != 32 / 8) {
|
||||
return true;
|
||||
}
|
||||
constexpr uint32_t SYSTEM_TRAY_ORIENTATION_HORZ = 0;
|
||||
return (*reinterpret_cast<uint32_t *>(data)) ==
|
||||
SYSTEM_TRAY_ORIENTATION_HORZ;
|
||||
}
|
||||
|
||||
void XCBTrayWindow::postCreateWindow() {
|
||||
if (ui_->ewmh()->_NET_WM_WINDOW_TYPE_DOCK &&
|
||||
ui_->ewmh()->_NET_WM_WINDOW_TYPE) {
|
||||
xcb_ewmh_set_wm_window_type(ui_->ewmh(), wid_, 1,
|
||||
&ui_->ewmh()->_NET_WM_WINDOW_TYPE_DOCK);
|
||||
}
|
||||
|
||||
if (ui_->ewmh()->_NET_WM_PID) {
|
||||
xcb_ewmh_set_wm_pid(ui_->ewmh(), wid_, getpid());
|
||||
}
|
||||
const char name[] = "Fcitx5 Tray Window";
|
||||
xcb_icccm_set_wm_name(ui_->connection(), wid_, XCB_ATOM_STRING, 8,
|
||||
sizeof(name) - 1, name);
|
||||
const char klass[] = "fcitx\0fcitx";
|
||||
xcb_icccm_set_wm_class(ui_->connection(), wid_, sizeof(klass) - 1, klass);
|
||||
|
||||
addEventMaskToWindow(
|
||||
ui_->connection(), wid_,
|
||||
XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_BUTTON_PRESS |
|
||||
XCB_EVENT_MASK_BUTTON_RELEASE | XCB_EVENT_MASK_STRUCTURE_NOTIFY |
|
||||
XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW |
|
||||
XCB_EVENT_MASK_VISIBILITY_CHANGE | XCB_EVENT_MASK_POINTER_MOTION);
|
||||
|
||||
if (trayDepth_ != 32) {
|
||||
// Change window attr.
|
||||
xcb_change_window_attributes_value_list_t list;
|
||||
list.background_pixmap = XCB_BACK_PIXMAP_PARENT_RELATIVE;
|
||||
xcb_screen_t *screen =
|
||||
xcb_aux_get_screen(ui_->connection(), ui_->defaultScreen());
|
||||
list.background_pixel = screen->white_pixel;
|
||||
list.border_pixel = screen->black_pixel;
|
||||
xcb_change_window_attributes_aux(
|
||||
ui_->connection(), wid_,
|
||||
XCB_CW_BACKING_PIXEL | XCB_CW_BORDER_PIXEL | XCB_CW_BACK_PIXMAP,
|
||||
&list);
|
||||
}
|
||||
}
|
||||
|
||||
void XCBTrayWindow::paint(cairo_t *c) {
|
||||
auto &theme = ui_->parent()->theme();
|
||||
auto *instance = ui_->parent()->instance();
|
||||
auto *ic = ui_->parent()->instance()->lastFocusedInputContext();
|
||||
std::string icon = "input-keyboard";
|
||||
std::string label;
|
||||
const InputMethodEntry *entry = nullptr;
|
||||
if (ic) {
|
||||
icon = instance->inputMethodIcon(ic);
|
||||
entry = instance->inputMethodEntry(ic);
|
||||
}
|
||||
if (entry) {
|
||||
label = entry->label();
|
||||
}
|
||||
|
||||
const auto &image = theme.loadImage(
|
||||
icon, label, std::min(height(), width()), ui_->parent());
|
||||
|
||||
cairo_save(c);
|
||||
cairo_set_operator(c, CAIRO_OPERATOR_CLEAR);
|
||||
cairo_paint(c);
|
||||
cairo_set_operator(c, CAIRO_OPERATOR_OVER);
|
||||
double scaleW = 1.0;
|
||||
double scaleH = 1.0;
|
||||
if (image.width() != width() || image.height() != height()) {
|
||||
scaleW = static_cast<double>(width()) / image.width();
|
||||
scaleH = static_cast<double>(height()) / image.height();
|
||||
if (scaleW > scaleH) {
|
||||
scaleH = scaleW;
|
||||
} else {
|
||||
scaleW = scaleH;
|
||||
}
|
||||
}
|
||||
int aw = scaleW * image.width();
|
||||
int ah = scaleH * image.height();
|
||||
|
||||
image.paintRegion(c, ThemeImage::TilePosition::Center, (width() - aw) / 2.0,
|
||||
(height() - ah) / 2.0, image.width() * scaleW,
|
||||
image.height() * scaleH);
|
||||
cairo_restore(c);
|
||||
}
|
||||
|
||||
void XCBTrayWindow::update() {
|
||||
if (!wid_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto *surface = prerender()) {
|
||||
cairo_t *c = cairo_create(surface);
|
||||
paint(c);
|
||||
cairo_destroy(c);
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
void XCBTrayWindow::render() {
|
||||
if (trayDepth_ != 32) {
|
||||
xcb_clear_area(ui_->connection(), false, wid_, 0, 0, width(), height());
|
||||
}
|
||||
auto *cr = cairo_create(surface_.get());
|
||||
if (trayDepth_ == 32) {
|
||||
cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
|
||||
cairo_paint(cr);
|
||||
}
|
||||
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
|
||||
cairo_set_source_surface(cr, contentSurface_.get(), 0, 0);
|
||||
cairo_paint(cr);
|
||||
cairo_destroy(cr);
|
||||
cairo_surface_flush(surface_.get());
|
||||
CLASSICUI_DEBUG() << "Render";
|
||||
}
|
||||
|
||||
void XCBTrayWindow::resume() {
|
||||
if (dockCallback_) {
|
||||
return;
|
||||
}
|
||||
char trayAtomNameBuf[100];
|
||||
sprintf(trayAtomNameBuf, "_NET_SYSTEM_TRAY_S%d", ui_->defaultScreen());
|
||||
xcb_screen_t *screen =
|
||||
xcb_aux_get_screen(ui_->connection(), ui_->defaultScreen());
|
||||
addEventMaskToWindow(ui_->connection(), screen->root,
|
||||
XCB_EVENT_MASK_STRUCTURE_NOTIFY);
|
||||
dockCallback_ = ui_->parent()->xcb()->call<IXCBModule::addSelection>(
|
||||
ui_->displayName(), trayAtomNameBuf,
|
||||
[this](xcb_atom_t) { refreshDockWindow(); });
|
||||
refreshDockWindow();
|
||||
}
|
||||
|
||||
void XCBTrayWindow::suspend() {
|
||||
if (!dockCallback_) {
|
||||
return;
|
||||
}
|
||||
dockCallback_.reset();
|
||||
destroyWindow();
|
||||
}
|
||||
|
||||
void XCBTrayWindow::updateMenu() {
|
||||
updateGroupMenu();
|
||||
|
||||
auto &imManager = ui_->parent()->instance()->inputMethodManager();
|
||||
if (imManager.groupCount() > 1) {
|
||||
menu_.insertAction(&separatorActions_[0], &groupAction_);
|
||||
} else {
|
||||
menu_.removeAction(&groupAction_);
|
||||
}
|
||||
updateInputMethodMenu();
|
||||
bool start = false;
|
||||
for (auto *action : menu_.actions()) {
|
||||
if (action == &separatorActions_[0]) {
|
||||
start = true;
|
||||
} else if (action == &configureAction_) {
|
||||
break;
|
||||
} else if (start) {
|
||||
menu_.removeAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
bool hasAction = false;
|
||||
if (auto *ic = ui_->parent()->instance()->mostRecentInputContext()) {
|
||||
auto &statusArea = ic->statusArea();
|
||||
for (auto *action : statusArea.allActions()) {
|
||||
if (!action->id()) {
|
||||
// Obviously it's not registered with ui manager.
|
||||
continue;
|
||||
}
|
||||
menu_.insertAction(&configureAction_, action);
|
||||
hasAction = true;
|
||||
}
|
||||
}
|
||||
if (hasAction) {
|
||||
menu_.insertAction(&configureAction_, &separatorActions_[1]);
|
||||
}
|
||||
}
|
||||
|
||||
void XCBTrayWindow::updateGroupMenu() {
|
||||
auto &imManager = ui_->parent()->instance()->inputMethodManager();
|
||||
std::vector<std::string> list = imManager.groups();
|
||||
groupActions_.clear();
|
||||
for (size_t i = 0; i < list.size(); i++) {
|
||||
const std::string &groupName = list[i];
|
||||
groupActions_.emplace_back();
|
||||
auto &groupAction = groupActions_.back();
|
||||
groupAction.setShortText(groupName);
|
||||
groupAction.connect<SimpleAction::Activated>(
|
||||
[&imManager, groupName](InputContext *) {
|
||||
imManager.setCurrentGroup(groupName);
|
||||
});
|
||||
groupAction.setCheckable(true);
|
||||
groupAction.setChecked(groupName == imManager.currentGroup().name());
|
||||
|
||||
auto &uiManager = ui_->parent()->instance()->userInterfaceManager();
|
||||
uiManager.registerAction(&groupAction);
|
||||
groupMenu_.addAction(&groupAction);
|
||||
}
|
||||
}
|
||||
|
||||
void XCBTrayWindow::updateInputMethodMenu() {
|
||||
auto &imManager = ui_->parent()->instance()->inputMethodManager();
|
||||
const auto &list = imManager.currentGroup().inputMethodList();
|
||||
inputMethodActions_.clear();
|
||||
auto *ic = ui_->parent()->instance()->mostRecentInputContext();
|
||||
for (size_t i = 0; i < list.size(); i++) {
|
||||
const auto *entry = imManager.entry(list[i].name());
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
inputMethodActions_.emplace_back();
|
||||
auto &inputMethodAction = inputMethodActions_.back();
|
||||
inputMethodAction.setShortText(entry->name());
|
||||
inputMethodAction.connect<SimpleAction::Activated>(
|
||||
[this, imName = entry->uniqueName()](InputContext *ic) {
|
||||
ui_->parent()->instance()->setCurrentInputMethod(ic, imName,
|
||||
false);
|
||||
});
|
||||
inputMethodAction.setCheckable(true);
|
||||
inputMethodAction.setChecked(
|
||||
ic ? (ui_->parent()->instance()->inputMethod(ic) ==
|
||||
entry->uniqueName())
|
||||
: false);
|
||||
|
||||
auto &uiManager = ui_->parent()->instance()->userInterfaceManager();
|
||||
uiManager.registerAction(&inputMethodAction);
|
||||
menu_.insertAction(&separatorActions_[0], &inputMethodAction);
|
||||
}
|
||||
}
|
||||
} // namespace fcitx::m3ui
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2017-2017 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#ifndef _FCITX_UI_CLASSIC_XCBTRAYWINDOW_H_
|
||||
#define _FCITX_UI_CLASSIC_XCBTRAYWINDOW_H_
|
||||
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <cairo.h>
|
||||
#include <xcb/xcb.h>
|
||||
#include <xcb/xproto.h>
|
||||
#include "fcitx-utils/handlertable.h"
|
||||
#include "fcitx/action.h"
|
||||
#include "fcitx/menu.h"
|
||||
#include "xcb_public.h"
|
||||
#include "xcbmenu.h"
|
||||
#include "xcbui.h"
|
||||
#include "xcbwindow.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
class XCBTrayWindow : public XCBWindow {
|
||||
public:
|
||||
XCBTrayWindow(XCBUI *ui);
|
||||
void initTray();
|
||||
|
||||
bool filterEvent(xcb_generic_event_t *event) override;
|
||||
void resume();
|
||||
void suspend();
|
||||
void update();
|
||||
void postCreateWindow() override;
|
||||
|
||||
void updateMenu();
|
||||
void updateGroupMenu();
|
||||
void updateInputMethodMenu();
|
||||
|
||||
void render() override;
|
||||
|
||||
private:
|
||||
void findDock();
|
||||
void sendTrayOpcode(long message, long data1, long data2, long data3);
|
||||
void refreshDockWindow();
|
||||
xcb_visualid_t trayVisual();
|
||||
bool trayOrientation();
|
||||
void paint(cairo_t *cr);
|
||||
void createTrayWindow();
|
||||
void resizeTrayWindow();
|
||||
|
||||
xcb_window_t dockWindow_ = XCB_WINDOW_NONE;
|
||||
std::unique_ptr<HandlerTableEntry<XCBSelectionNotifyCallback>>
|
||||
dockCallback_;
|
||||
|
||||
xcb_atom_t atoms_[5];
|
||||
|
||||
MenuPool menuPool_;
|
||||
|
||||
Menu menu_;
|
||||
SimpleAction groupAction_;
|
||||
SimpleAction separatorActions_[2];
|
||||
SimpleAction configureAction_;
|
||||
SimpleAction restartAction_;
|
||||
SimpleAction exitAction_;
|
||||
|
||||
#if 0
|
||||
SimpleAction testAction1_;
|
||||
SimpleAction testAction2_;
|
||||
Menu testMenu1_;
|
||||
Menu testMenu2_;
|
||||
SimpleAction testSubAction1_;
|
||||
SimpleAction testSubAction2_;
|
||||
#endif
|
||||
|
||||
xcb_visualid_t trayVid_ = 0;
|
||||
int trayDepth_ = 0;
|
||||
|
||||
bool isHorizontal_ = true;
|
||||
int hintWidth_ = 0;
|
||||
int hintHeight_ = 0;
|
||||
|
||||
Menu groupMenu_;
|
||||
std::list<SimpleAction> groupActions_;
|
||||
std::list<SimpleAction> inputMethodActions_;
|
||||
};
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
|
||||
#endif // _FCITX_UI_CLASSIC_XCBTRAYWINDOW_H_
|
||||
@@ -0,0 +1,834 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2016-2016 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
|
||||
#include "xcbui.h"
|
||||
#include <sys/types.h>
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
#include <cairo.h>
|
||||
#include <xcb/randr.h>
|
||||
#include <xcb/xcb.h>
|
||||
#include <xcb/xcb_aux.h>
|
||||
#include <xcb/xinerama.h>
|
||||
#include <xcb/xproto.h>
|
||||
#include "fcitx-utils/endian_p.h"
|
||||
#include "fcitx-utils/eventloopinterface.h"
|
||||
#include "fcitx-utils/macros.h"
|
||||
#include "fcitx-utils/misc.h"
|
||||
#include "fcitx-utils/rect.h"
|
||||
#include "fcitx/userinterface.h"
|
||||
#include "classicui.h"
|
||||
#include "common.h"
|
||||
#include "xcb_public.h"
|
||||
#include "xcbinputwindow.h"
|
||||
#include "xcbtraywindow.h"
|
||||
#include "xcbwindow.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
void addEventMaskToWindow(xcb_connection_t *conn, xcb_window_t wid,
|
||||
uint32_t mask) {
|
||||
auto get_attr_cookie = xcb_get_window_attributes(conn, wid);
|
||||
auto get_attr_reply = makeUniqueCPtr(
|
||||
xcb_get_window_attributes_reply(conn, get_attr_cookie, nullptr));
|
||||
if (get_attr_reply && (get_attr_reply->your_event_mask & mask) != mask) {
|
||||
const uint32_t newMask = get_attr_reply->your_event_mask | mask;
|
||||
xcb_change_window_attributes(conn, wid, XCB_CW_EVENT_MASK, &newMask);
|
||||
}
|
||||
}
|
||||
|
||||
xcb_visualid_t findVisual(xcb_screen_t *screen) {
|
||||
auto *visual = xcb_aux_find_visual_by_attrs(screen, -1, 32);
|
||||
if (!visual) {
|
||||
return screen->root_visual;
|
||||
}
|
||||
return visual->visual_id;
|
||||
}
|
||||
|
||||
XCBFontOption forcedDpi(xcb_connection_t *conn, xcb_screen_t *screen) {
|
||||
int offset = 0;
|
||||
std::vector<char> resources;
|
||||
bool more = true;
|
||||
do {
|
||||
auto cookie = xcb_get_property(conn, false, screen->root,
|
||||
XCB_ATOM_RESOURCE_MANAGER,
|
||||
XCB_ATOM_STRING, offset / 4, 8192);
|
||||
auto reply =
|
||||
makeUniqueCPtr(xcb_get_property_reply(conn, cookie, nullptr));
|
||||
more = false;
|
||||
if (reply && reply->format == 8 && reply->type == XCB_ATOM_STRING) {
|
||||
const auto *start =
|
||||
static_cast<const char *>(xcb_get_property_value(reply.get()));
|
||||
const auto *end =
|
||||
start + xcb_get_property_value_length(reply.get());
|
||||
resources.insert(resources.end(), start, end);
|
||||
offset += xcb_get_property_value_length(reply.get());
|
||||
more = reply->bytes_after != 0;
|
||||
}
|
||||
} while (more);
|
||||
|
||||
XCBFontOption option;
|
||||
auto parse = [](const std::vector<char> &resources, const char *item,
|
||||
auto callback) {
|
||||
auto iter = resources.begin();
|
||||
auto end = resources.end();
|
||||
while (iter < end) {
|
||||
auto next = std::find(iter, end, '\n');
|
||||
int cLen = strlen(item);
|
||||
if (next - iter > cLen && std::equal(iter, iter + cLen, item)) {
|
||||
std::string value(iter + cLen, next);
|
||||
callback(value);
|
||||
}
|
||||
|
||||
iter = std::next(next);
|
||||
}
|
||||
};
|
||||
parse(resources, "Xft.dpi:\t", [&option](const std::string &value) {
|
||||
std::from_chars(value.data(), value.data() + value.size(), option.dpi);
|
||||
});
|
||||
parse(resources, "Xft.antialias:\t", [&option](const std::string &value) {
|
||||
int antialias = 0;
|
||||
if (std::from_chars(value.data(), value.data() + value.size(),
|
||||
antialias)
|
||||
.ec == std::errc()) {
|
||||
option.antialias = antialias;
|
||||
}
|
||||
});
|
||||
parse(resources, "Xft.hintstyle:\t", [&option](const std::string &value) {
|
||||
if (value == "hintfull") {
|
||||
option.hint = XCBHintStyle::Full;
|
||||
} else if (value == "hintnone") {
|
||||
option.hint = XCBHintStyle::NoHint;
|
||||
} else if (value == "hintmedium") {
|
||||
option.hint = XCBHintStyle::Medium;
|
||||
} else if (value == "hintslight") {
|
||||
option.hint = XCBHintStyle::Slight;
|
||||
}
|
||||
// default
|
||||
});
|
||||
parse(resources, "Xft.rgba:\t", [&option](const std::string &value) {
|
||||
if (value == "none") {
|
||||
option.rgba = XCBRGBA::NoRGBA;
|
||||
} else if (value == "rgb") {
|
||||
option.rgba = XCBRGBA::RGB;
|
||||
} else if (value == "bgr") {
|
||||
option.rgba = XCBRGBA::BGR;
|
||||
} else if (value == "vrgb") {
|
||||
option.rgba = XCBRGBA::VRGB;
|
||||
} else if (value == "vbgr") {
|
||||
option.rgba = XCBRGBA::VBGR;
|
||||
}
|
||||
// default
|
||||
});
|
||||
return option;
|
||||
}
|
||||
|
||||
void XCBFontOption::setupPangoContext(PangoContext *context) const {
|
||||
cairo_hint_style_t hint = CAIRO_HINT_STYLE_DEFAULT;
|
||||
cairo_antialias_t aa = CAIRO_ANTIALIAS_DEFAULT;
|
||||
cairo_subpixel_order_t subpixel = CAIRO_SUBPIXEL_ORDER_DEFAULT;
|
||||
switch (this->hint) {
|
||||
case XCBHintStyle::NoHint:
|
||||
hint = CAIRO_HINT_STYLE_NONE;
|
||||
break;
|
||||
case XCBHintStyle::Slight:
|
||||
hint = CAIRO_HINT_STYLE_SLIGHT;
|
||||
break;
|
||||
case XCBHintStyle::Medium:
|
||||
hint = CAIRO_HINT_STYLE_MEDIUM;
|
||||
break;
|
||||
case XCBHintStyle::Full:
|
||||
hint = CAIRO_HINT_STYLE_FULL;
|
||||
break;
|
||||
default:
|
||||
hint = CAIRO_HINT_STYLE_DEFAULT;
|
||||
break;
|
||||
}
|
||||
switch (rgba) {
|
||||
case XCBRGBA::NoRGBA:
|
||||
subpixel = CAIRO_SUBPIXEL_ORDER_DEFAULT;
|
||||
break;
|
||||
case XCBRGBA::RGB:
|
||||
subpixel = CAIRO_SUBPIXEL_ORDER_RGB;
|
||||
break;
|
||||
case XCBRGBA::BGR:
|
||||
subpixel = CAIRO_SUBPIXEL_ORDER_BGR;
|
||||
break;
|
||||
case XCBRGBA::VRGB:
|
||||
subpixel = CAIRO_SUBPIXEL_ORDER_VRGB;
|
||||
break;
|
||||
case XCBRGBA::VBGR:
|
||||
subpixel = CAIRO_SUBPIXEL_ORDER_VBGR;
|
||||
break;
|
||||
default:
|
||||
subpixel = CAIRO_SUBPIXEL_ORDER_DEFAULT;
|
||||
break;
|
||||
}
|
||||
|
||||
if (antialias) {
|
||||
if (subpixel != CAIRO_SUBPIXEL_ORDER_DEFAULT) {
|
||||
aa = CAIRO_ANTIALIAS_SUBPIXEL;
|
||||
} else {
|
||||
aa = CAIRO_ANTIALIAS_GRAY;
|
||||
}
|
||||
} else {
|
||||
aa = CAIRO_ANTIALIAS_NONE;
|
||||
}
|
||||
|
||||
auto *options = cairo_font_options_create();
|
||||
cairo_font_options_set_hint_style(options, hint);
|
||||
cairo_font_options_set_subpixel_order(options, subpixel);
|
||||
cairo_font_options_set_antialias(options, aa);
|
||||
cairo_font_options_set_hint_metrics(options, CAIRO_HINT_METRICS_ON);
|
||||
pango_cairo_context_set_font_options(context, options);
|
||||
cairo_font_options_destroy(options);
|
||||
}
|
||||
|
||||
XCBUI::XCBUI(M3UI *parent, const std::string &name, xcb_connection_t *conn,
|
||||
int defaultScreen)
|
||||
: UIInterface("x11:" + name), parent_(parent), displayName_(name),
|
||||
conn_(conn), defaultScreen_(defaultScreen) {
|
||||
ewmh_ = parent_->xcb()->call<IXCBModule::ewmh>(displayName_);
|
||||
inputWindow_ = std::make_unique<XCBInputWindow>(this);
|
||||
trayWindow_ = std::make_unique<XCBTrayWindow>(this);
|
||||
|
||||
compMgrAtomString_ = "_NET_WM_CM_S" + std::to_string(defaultScreen_);
|
||||
compMgrAtom_ = parent_->xcb()->call<IXCBModule::atom>(
|
||||
displayName_, compMgrAtomString_, false);
|
||||
|
||||
auto xsettingsSelectionString =
|
||||
"_XSETTINGS_S" + std::to_string(defaultScreen_);
|
||||
managerAtom_ =
|
||||
parent_->xcb()->call<IXCBModule::atom>(displayName_, "MANAGER", false);
|
||||
xsettingsSelectionAtom_ = parent_->xcb()->call<IXCBModule::atom>(
|
||||
displayName_, xsettingsSelectionString, false);
|
||||
xsettingsAtom_ = parent_->xcb()->call<IXCBModule::atom>(
|
||||
displayName_, "_XSETTINGS_SETTINGS", false);
|
||||
|
||||
initScreenEvent_ = parent_->instance()->eventLoop().addTimeEvent(
|
||||
CLOCK_MONOTONIC, now(CLOCK_MONOTONIC) + 10000, 0,
|
||||
[this](EventSourceTime *, uint64_t) {
|
||||
initScreen();
|
||||
return true;
|
||||
});
|
||||
initScreenEvent_->setEnabled(false);
|
||||
|
||||
eventHandlers_.emplace_back(parent_->xcb()->call<IXCBModule::addSelection>(
|
||||
name, compMgrAtomString_,
|
||||
[this](xcb_atom_t) { refreshCompositeManager(); }));
|
||||
|
||||
eventHandlers_.emplace_back(
|
||||
parent_->xcb()->call<IXCBModule::addEventFilter>(
|
||||
name, [this](xcb_connection_t *, xcb_generic_event_t *event) {
|
||||
uint8_t response_type = event->response_type & ~0x80;
|
||||
switch (response_type) {
|
||||
case XCB_CLIENT_MESSAGE: {
|
||||
auto *client_message =
|
||||
reinterpret_cast<xcb_client_message_event_t *>(event);
|
||||
if (client_message->data.data32[1] == compMgrAtom_) {
|
||||
refreshCompositeManager();
|
||||
} else if (client_message->type == managerAtom_ &&
|
||||
client_message->data.data32[1] ==
|
||||
xsettingsSelectionAtom_) {
|
||||
CLASSICUI_DEBUG() << "Refresh manager";
|
||||
refreshManager();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case XCB_DESTROY_NOTIFY: {
|
||||
auto *destroy =
|
||||
reinterpret_cast<xcb_destroy_notify_event_t *>(event);
|
||||
if (destroy->window == xsettingsWindow_) {
|
||||
refreshManager();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case XCB_PROPERTY_NOTIFY: {
|
||||
auto *property =
|
||||
reinterpret_cast<xcb_property_notify_event_t *>(event);
|
||||
if (xsettingsWindow_ &&
|
||||
property->window == xsettingsWindow_) {
|
||||
readXSettings();
|
||||
}
|
||||
|
||||
auto *screen = xcb_aux_get_screen(conn_, defaultScreen_);
|
||||
if (property->window == screen->root &&
|
||||
property->atom == XCB_ATOM_RESOURCE_MANAGER) {
|
||||
fontOption_ = forcedDpi(conn_, screen);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case XCB_CONFIGURE_NOTIFY: {
|
||||
auto *configure =
|
||||
reinterpret_cast<xcb_configure_notify_event_t *>(event);
|
||||
auto *screen = xcb_aux_get_screen(conn_, defaultScreen_);
|
||||
if (configure->window == screen->root) {
|
||||
scheduleUpdateScreen();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (multiScreen_ == MultiScreenExtension::Randr) {
|
||||
if (response_type ==
|
||||
xrandrFirstEvent_ + XCB_RANDR_SCREEN_CHANGE_NOTIFY) {
|
||||
scheduleUpdateScreen();
|
||||
} else if (response_type ==
|
||||
xrandrFirstEvent_ + XCB_RANDR_NOTIFY) {
|
||||
|
||||
auto *randr =
|
||||
reinterpret_cast<xcb_randr_notify_event_t *>(event);
|
||||
if (randr->subCode == XCB_RANDR_NOTIFY_CRTC_CHANGE ||
|
||||
randr->subCode == XCB_RANDR_NOTIFY_OUTPUT_CHANGE) {
|
||||
scheduleUpdateScreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}));
|
||||
|
||||
xcb_screen_t *screen = xcb_aux_get_screen(conn_, defaultScreen_);
|
||||
addEventMaskToWindow(conn_, screen->root,
|
||||
XCB_EVENT_MASK_STRUCTURE_NOTIFY |
|
||||
XCB_EVENT_MASK_PROPERTY_CHANGE);
|
||||
root_ = screen->root;
|
||||
fontOption_ = forcedDpi(conn_, screen);
|
||||
CLASSICUI_DEBUG() << "Xft.dpi: " << fontOption_.dpi;
|
||||
initScreen();
|
||||
refreshCompositeManager();
|
||||
trayWindow_->initTray();
|
||||
refreshManager();
|
||||
}
|
||||
|
||||
XCBUI::~XCBUI() {
|
||||
inputWindow_.reset();
|
||||
trayWindow_.reset();
|
||||
device_.reset();
|
||||
}
|
||||
|
||||
void XCBUI::initScreen() {
|
||||
auto *screen = xcb_aux_get_screen(conn_, defaultScreen_);
|
||||
int newScreenCount = xcb_setup_roots_length(xcb_get_setup(conn_));
|
||||
if (multiScreen_ == MultiScreenExtension::EXTNone && newScreenCount == 1) {
|
||||
const xcb_query_extension_reply_t *reply =
|
||||
xcb_get_extension_data(conn_, &xcb_randr_id);
|
||||
if (reply && reply->present) {
|
||||
multiScreen_ = MultiScreenExtension::Randr;
|
||||
xrandrFirstEvent_ = reply->first_event;
|
||||
xcb_randr_select_input(conn_, screen->root,
|
||||
XCB_RANDR_NOTIFY_MASK_SCREEN_CHANGE |
|
||||
XCB_RANDR_NOTIFY_MASK_OUTPUT_CHANGE |
|
||||
XCB_RANDR_NOTIFY_MASK_CRTC_CHANGE |
|
||||
XCB_RANDR_NOTIFY_MASK_OUTPUT_PROPERTY);
|
||||
} else {
|
||||
const xcb_query_extension_reply_t *reply =
|
||||
xcb_get_extension_data(conn_, &xcb_xinerama_id);
|
||||
|
||||
if (reply && reply->present) {
|
||||
multiScreen_ = MultiScreenExtension::Xinerama;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
maxDpi_ = -1;
|
||||
rects_.clear();
|
||||
if (multiScreen_ == MultiScreenExtension::Randr) {
|
||||
auto cookie =
|
||||
xcb_randr_get_screen_resources_current(conn_, screen->root);
|
||||
auto reply =
|
||||
makeUniqueCPtr(xcb_randr_get_screen_resources_current_reply(
|
||||
conn_, cookie, nullptr));
|
||||
if (reply) {
|
||||
xcb_timestamp_t timestamp = 0;
|
||||
xcb_randr_output_t *outputs = nullptr;
|
||||
int outputCount =
|
||||
xcb_randr_get_screen_resources_current_outputs_length(
|
||||
reply.get());
|
||||
|
||||
if (outputCount) {
|
||||
timestamp = reply->config_timestamp;
|
||||
outputs =
|
||||
xcb_randr_get_screen_resources_current_outputs(reply.get());
|
||||
} else {
|
||||
auto resourcesCookie =
|
||||
xcb_randr_get_screen_resources(conn_, screen->root);
|
||||
auto resourcesReply =
|
||||
makeUniqueCPtr(xcb_randr_get_screen_resources_reply(
|
||||
conn_, resourcesCookie, nullptr));
|
||||
if (resourcesReply) {
|
||||
timestamp = resourcesReply->config_timestamp;
|
||||
outputCount = xcb_randr_get_screen_resources_outputs_length(
|
||||
resourcesReply.get());
|
||||
outputs = xcb_randr_get_screen_resources_outputs(
|
||||
resourcesReply.get());
|
||||
}
|
||||
}
|
||||
|
||||
if (outputCount) {
|
||||
auto primaryCookie =
|
||||
xcb_randr_get_output_primary(conn_, screen->root);
|
||||
auto primary =
|
||||
makeUniqueCPtr(xcb_randr_get_output_primary_reply(
|
||||
conn_, primaryCookie, nullptr));
|
||||
if (primary) {
|
||||
for (int i = 0; i < outputCount; i++) {
|
||||
auto outputInfoCookie = xcb_randr_get_output_info(
|
||||
conn_, outputs[i], timestamp);
|
||||
auto output =
|
||||
makeUniqueCPtr(xcb_randr_get_output_info_reply(
|
||||
conn_, outputInfoCookie, nullptr));
|
||||
// Invalid, disconnected or disabled output
|
||||
if (!output) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (output->connection !=
|
||||
XCB_RANDR_CONNECTION_CONNECTED) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (output->crtc == XCB_NONE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto crtcCookie = xcb_randr_get_crtc_info(
|
||||
conn_, output->crtc, output->timestamp);
|
||||
auto crtc =
|
||||
makeUniqueCPtr(xcb_randr_get_crtc_info_reply(
|
||||
conn_, crtcCookie, nullptr));
|
||||
if (crtc) {
|
||||
Rect rect;
|
||||
rect.setPosition(crtc->x, crtc->y);
|
||||
auto outputWidth = output->mm_width;
|
||||
auto outputHeight = output->mm_height;
|
||||
if (crtc->rotation ==
|
||||
XCB_RANDR_ROTATION_ROTATE_90 ||
|
||||
crtc->rotation ==
|
||||
XCB_RANDR_ROTATION_ROTATE_270) {
|
||||
std::swap(outputWidth, outputHeight);
|
||||
}
|
||||
int dpiX = 25.4 * crtc->width / outputWidth;
|
||||
int dpiY = 25.4 * crtc->height / outputHeight;
|
||||
rect.setSize(crtc->width, crtc->height);
|
||||
rects_.emplace_back(rect, std::min(dpiX, dpiY));
|
||||
maxDpi_ = std::max(maxDpi_, rects_.back().second);
|
||||
|
||||
if (outputs[i] == primary->output) {
|
||||
primaryDpi_ = rects_.back().second;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if (multiScreen_ == MultiScreenExtension::Xinerama) {
|
||||
auto cookie = xcb_xinerama_query_screens(conn_);
|
||||
if (auto reply = makeUniqueCPtr(
|
||||
xcb_xinerama_query_screens_reply(conn_, cookie, nullptr))) {
|
||||
xcb_xinerama_screen_info_iterator_t iter;
|
||||
for (iter = xcb_xinerama_query_screens_screen_info_iterator(
|
||||
reply.get());
|
||||
iter.rem; xcb_xinerama_screen_info_next(&iter)) {
|
||||
auto *info = iter.data;
|
||||
auto x = info->x_org;
|
||||
auto y = info->y_org;
|
||||
auto w = info->width;
|
||||
auto h = info->height;
|
||||
rects_.emplace_back(Rect().setPosition(x, y).setSize(w, h), -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rects_.empty()) {
|
||||
rects_.emplace_back(
|
||||
Rect(0, 0, screen->width_in_pixels, screen->height_in_pixels), -1);
|
||||
}
|
||||
|
||||
screenDpi_ =
|
||||
25.4 * screen->height_in_pixels / screen->height_in_millimeters;
|
||||
|
||||
CLASSICUI_DEBUG() << "Screen rects are: " << rects_
|
||||
<< " Primary DPI: " << primaryDpi_
|
||||
<< " XScreen DPI: " << screenDpi_;
|
||||
}
|
||||
|
||||
void XCBUI::refreshCompositeManager() {
|
||||
auto cookie = xcb_get_selection_owner(conn_, compMgrAtom_);
|
||||
auto reply =
|
||||
makeUniqueCPtr(xcb_get_selection_owner_reply(conn_, cookie, nullptr));
|
||||
if (reply) {
|
||||
compMgrWindow_ = reply->owner;
|
||||
}
|
||||
|
||||
xcb_screen_t *screen = xcb_aux_get_screen(conn_, defaultScreen_);
|
||||
if (needFreeColorMap_) {
|
||||
xcb_free_colormap(conn_, colorMap_);
|
||||
}
|
||||
|
||||
if (compMgrWindow_) {
|
||||
addEventMaskToWindow(conn_, compMgrWindow_,
|
||||
XCB_EVENT_MASK_STRUCTURE_NOTIFY);
|
||||
colorMap_ = xcb_generate_id(conn_);
|
||||
xcb_create_colormap(conn_, XCB_COLORMAP_ALLOC_NONE, colorMap_,
|
||||
screen->root, visualId());
|
||||
needFreeColorMap_ = true;
|
||||
} else {
|
||||
colorMap_ = screen->default_colormap;
|
||||
needFreeColorMap_ = false;
|
||||
}
|
||||
CLASSICUI_DEBUG() << "Refresh color map: " << colorMap_
|
||||
<< " vid: " << visualId()
|
||||
<< " CompMgr: " << compMgrWindow_;
|
||||
inputWindow_->createWindow(visualId());
|
||||
// mainWindow_->createWindow();
|
||||
}
|
||||
|
||||
void XCBUI::refreshManager() {
|
||||
xcb_grab_server(conn_);
|
||||
auto cookie = xcb_get_selection_owner(conn_, xsettingsSelectionAtom_);
|
||||
auto reply =
|
||||
makeUniqueCPtr(xcb_get_selection_owner_reply(conn_, cookie, nullptr));
|
||||
if (reply) {
|
||||
xsettingsWindow_ = reply->owner;
|
||||
}
|
||||
if (xsettingsWindow_) {
|
||||
addEventMaskToWindow(conn_, xsettingsWindow_,
|
||||
XCB_EVENT_MASK_STRUCTURE_NOTIFY |
|
||||
XCB_EVENT_MASK_PROPERTY_CHANGE);
|
||||
}
|
||||
xcb_ungrab_server(conn_);
|
||||
|
||||
readXSettings();
|
||||
}
|
||||
|
||||
void XCBUI::readXSettings() {
|
||||
if (!xsettingsWindow_) {
|
||||
return;
|
||||
}
|
||||
|
||||
xcb_grab_server(conn_);
|
||||
int offset = 0;
|
||||
std::vector<char> data;
|
||||
bool more = true;
|
||||
bool error = false;
|
||||
do {
|
||||
auto cookie =
|
||||
xcb_get_property(conn_, false, xsettingsWindow_, xsettingsAtom_,
|
||||
xsettingsAtom_, offset / 4, 10);
|
||||
auto reply =
|
||||
makeUniqueCPtr(xcb_get_property_reply(conn_, cookie, nullptr));
|
||||
more = false;
|
||||
if (reply && reply->format == 8 && reply->type == xsettingsAtom_) {
|
||||
const auto *start =
|
||||
static_cast<const char *>(xcb_get_property_value(reply.get()));
|
||||
const auto *end =
|
||||
start + xcb_get_property_value_length(reply.get());
|
||||
data.insert(data.end(), start, end);
|
||||
offset += xcb_get_property_value_length(reply.get());
|
||||
more = reply->bytes_after != 0;
|
||||
}
|
||||
if (!reply) {
|
||||
error = true;
|
||||
}
|
||||
} while (more);
|
||||
xcb_ungrab_server(conn_);
|
||||
|
||||
if (error || data.empty()) {
|
||||
return;
|
||||
}
|
||||
auto byteOrder = hostByteOrder();
|
||||
if (data[0] != BYTE_ORDER_LSB_FIRST && data[0] != BYTE_ORDER_MSB_FIRST) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool needSwap = byteOrder != data[0];
|
||||
auto iter = data.cbegin();
|
||||
auto readCard32 = [needSwap, &data, &iter](uint32_t *result) {
|
||||
if (std::distance(iter, data.cend()) <
|
||||
static_cast<ssize_t>(sizeof(uint32_t))) {
|
||||
return false;
|
||||
}
|
||||
uint32_t x = *reinterpret_cast<const uint32_t *>(&(*iter));
|
||||
|
||||
if (needSwap) {
|
||||
*result = (x << 24) | ((x & 0xff00) << 8) | ((x & 0xff0000) >> 8) |
|
||||
(x >> 24);
|
||||
} else {
|
||||
*result = x;
|
||||
}
|
||||
iter += sizeof(uint32_t);
|
||||
return true;
|
||||
};
|
||||
auto readCard16 = [needSwap, &data, &iter](uint16_t *result) {
|
||||
if (std::distance(iter, data.cend()) <
|
||||
static_cast<ssize_t>(sizeof(uint16_t))) {
|
||||
return false;
|
||||
}
|
||||
uint16_t x = *reinterpret_cast<const uint16_t *>(&(*iter));
|
||||
|
||||
if (needSwap) {
|
||||
*result = (x << 8) | (x >> 8);
|
||||
} else {
|
||||
*result = x;
|
||||
}
|
||||
iter += sizeof(uint16_t);
|
||||
return true;
|
||||
};
|
||||
auto readCard8 = [&data, &iter](uint8_t *result) {
|
||||
if (std::distance(iter, data.cend()) <
|
||||
static_cast<ssize_t>(sizeof(uint8_t))) {
|
||||
return false;
|
||||
}
|
||||
uint8_t x = *reinterpret_cast<const uint8_t *>(&(*iter));
|
||||
*result = x;
|
||||
iter += sizeof(uint8_t);
|
||||
return true;
|
||||
};
|
||||
// 1 CARD8 byte-order
|
||||
// 3 unused
|
||||
// 4 CARD32 SERIAL
|
||||
// 4 CARD32 N_SETTINGS
|
||||
uint32_t dummy;
|
||||
if (!readCard32(&dummy)) {
|
||||
return;
|
||||
}
|
||||
// SERIAL
|
||||
if (!readCard32(&dummy)) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t nSettings;
|
||||
if (!readCard32(&nSettings)) {
|
||||
return;
|
||||
}
|
||||
if (static_cast<uint64_t>(nSettings) * 8 + 8 > data.size()) {
|
||||
return;
|
||||
}
|
||||
for (uint32_t i = 0; i < nSettings; i++) {
|
||||
// 1 SETTING_TYPE type
|
||||
// 1 unused
|
||||
// 2 n name-len
|
||||
// n STRING8 name
|
||||
// P unused, p=pad(n)
|
||||
// 4 CARD32 last-change-serial
|
||||
uint8_t type;
|
||||
if (!readCard8(&type)) {
|
||||
return;
|
||||
}
|
||||
// Valid types are 0,1,2.
|
||||
if (type > 2) {
|
||||
return;
|
||||
}
|
||||
// Unused
|
||||
uint8_t dummy8;
|
||||
if (!readCard8(&dummy8)) {
|
||||
return;
|
||||
}
|
||||
uint16_t nameLen;
|
||||
if (!readCard16(&nameLen)) {
|
||||
return;
|
||||
}
|
||||
#define XSETTINGS_PAD(n, m) (((n) + (m) - 1) & (~((m) - 1)))
|
||||
uint32_t namePad = XSETTINGS_PAD(nameLen, 4);
|
||||
if (std::distance(iter, data.cend()) < namePad) {
|
||||
return;
|
||||
}
|
||||
std::string_view name(&(*iter), nameLen);
|
||||
iter += namePad;
|
||||
if (!readCard32(&dummy)) {
|
||||
return;
|
||||
}
|
||||
switch (type) {
|
||||
case 0: // Integer
|
||||
if (!readCard32(&dummy)) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 1: {
|
||||
// String
|
||||
uint32_t len;
|
||||
if (!readCard32(&len)) {
|
||||
return;
|
||||
}
|
||||
uint32_t lenPad = XSETTINGS_PAD(len, 4);
|
||||
if (std::distance(iter, data.cend()) < lenPad) {
|
||||
return;
|
||||
}
|
||||
std::string_view value(&(*iter), len);
|
||||
iter += lenPad;
|
||||
if (name == "Net/IconThemeName" && !value.empty()) {
|
||||
iconThemeName_ = value;
|
||||
if (parent()->theme().setIconTheme(iconThemeName_)) {
|
||||
trayWindow_->update();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 2: // Color
|
||||
// 4 card 16, just do it with 2 card32
|
||||
if (!readCard32(&dummy)) {
|
||||
return;
|
||||
}
|
||||
if (!readCard32(&dummy)) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
xcb_visualid_t XCBUI::visualId() const {
|
||||
xcb_screen_t *screen = xcb_aux_get_screen(conn_, defaultScreen_);
|
||||
if (compMgrWindow_) {
|
||||
return findVisual(screen);
|
||||
}
|
||||
return screen->root_visual;
|
||||
}
|
||||
|
||||
void XCBUI::update(UserInterfaceComponent component,
|
||||
InputContext *inputContext) {
|
||||
if (component == UserInterfaceComponent::InputPanel) {
|
||||
inputWindow_->update(inputContext);
|
||||
}
|
||||
}
|
||||
|
||||
void XCBUI::updateCursor(InputContext *inputContext) {
|
||||
inputWindow_->updatePosition(inputContext);
|
||||
}
|
||||
|
||||
void XCBUI::updateCurrentInputMethod(InputContext *inputContext) {
|
||||
FCITX_UNUSED(inputContext);
|
||||
trayWindow_->update();
|
||||
}
|
||||
|
||||
int XCBUI::dpiByPosition(int x, int y) {
|
||||
int shortestDistance = std::numeric_limits<int>::max();
|
||||
int screenDpi = -1;
|
||||
for (const auto &rect : screenRects()) {
|
||||
int thisDistance = rect.first.distance(x, y);
|
||||
if (thisDistance < shortestDistance) {
|
||||
shortestDistance = thisDistance;
|
||||
screenDpi = rect.second;
|
||||
}
|
||||
}
|
||||
|
||||
return scaledDPI(screenDpi);
|
||||
}
|
||||
|
||||
int XCBUI::scaledDPI(int dpi) {
|
||||
if (!*parent_->config().perScreenDPI ||
|
||||
parent_->xcb()->call<IXCBModule::isXWayland>(displayName_)) {
|
||||
// CLASSICUI_DEBUG() << "Use font option dpi: " << fontOption_.dpi;
|
||||
if (fontOption_.dpi > 0) {
|
||||
return fontOption_.dpi;
|
||||
}
|
||||
if (screenDpi_ >= 96) {
|
||||
// Nowadays their should not be tiny dpi screen I assume.
|
||||
// In ancient days, there used to be invalid DPI value that make
|
||||
// font extremely tiny.
|
||||
return screenDpi_;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if (dpi < 0) {
|
||||
return fontOption_.dpi;
|
||||
}
|
||||
|
||||
double targetDPI;
|
||||
auto baseScreenDPI = primaryDpi_ > 0 ? primaryDpi_ : maxDpi_;
|
||||
auto baseDPI = fontOption_.dpi > 0 ? fontOption_.dpi : screenDpi_;
|
||||
targetDPI = (static_cast<double>(dpi) / baseScreenDPI) * baseDPI;
|
||||
double scale = targetDPI / 96;
|
||||
if (scale < 1) {
|
||||
targetDPI = 96;
|
||||
}
|
||||
return targetDPI;
|
||||
}
|
||||
|
||||
void XCBUI::resume() {}
|
||||
|
||||
void XCBUI::suspend() {
|
||||
inputWindow_->update(nullptr);
|
||||
trayWindow_->suspend();
|
||||
}
|
||||
|
||||
void XCBUI::setEnableTray(bool enable) {
|
||||
bool enableTray = enable && !parent_->suspended();
|
||||
if (enableTray) {
|
||||
trayWindow_->resume();
|
||||
} else {
|
||||
trayWindow_->suspend();
|
||||
}
|
||||
}
|
||||
|
||||
void XCBUI::scheduleUpdateScreen() {
|
||||
initScreenEvent_->setNextInterval(100000);
|
||||
initScreenEvent_->setOneShot();
|
||||
}
|
||||
|
||||
bool XCBUI::grabPointer(XCBWindow *window) {
|
||||
auto cookie = xcb_grab_pointer(
|
||||
conn_, false, window->wid(),
|
||||
(XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE |
|
||||
XCB_EVENT_MASK_BUTTON_MOTION | XCB_EVENT_MASK_ENTER_WINDOW |
|
||||
XCB_EVENT_MASK_LEAVE_WINDOW | XCB_EVENT_MASK_POINTER_MOTION),
|
||||
XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC, XCB_WINDOW_NONE,
|
||||
XCB_CURSOR_NONE, XCB_TIME_CURRENT_TIME);
|
||||
|
||||
auto reply = makeUniqueCPtr(xcb_grab_pointer_reply(conn_, cookie, nullptr));
|
||||
if (reply && reply->status == XCB_GRAB_STATUS_SUCCESS) {
|
||||
pointerGrabber_ = window;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void XCBUI::ungrabPointer() {
|
||||
if (pointerGrabber_) {
|
||||
xcb_ungrab_pointer(conn_, XCB_TIME_CURRENT_TIME);
|
||||
pointerGrabber_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void XCBUI::destroyCairoDevice(cairo_device_t *device) {
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
// Here's how cairo-xcb works.
|
||||
// When a new xcb connection is seen, a internal shared xcb device is
|
||||
// created and referenced by cairo itself. When device_finish is called,
|
||||
// this internal shared reference is decreased, and will be destroyed when
|
||||
// ref is 0. So while it looks like we reference and dereference which
|
||||
// should does nothing, the cairo_device_finish will to the actual work to
|
||||
// clean up the internal reference. See also:
|
||||
// https://lists.cairographics.org/archives/cairo/2018-November/028791.html
|
||||
// Such design is to allow xcb shared connection data to be kept even if
|
||||
// there is no xcb surface. Though this API design really sucks, since there
|
||||
// is no API to create a xcb device and pass it to cairo_xcb_surface_create.
|
||||
// Instead, we have to get the device pointer from a xcb surface.
|
||||
cairo_device_finish(device);
|
||||
cairo_device_destroy(device);
|
||||
}
|
||||
|
||||
void XCBUI::setCairoDevice(cairo_device_t *device) {
|
||||
if (device_.get() != device) {
|
||||
device_.reset();
|
||||
device_.reset(cairo_device_reference(device));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2016-2016 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#ifndef _FCITX_UI_CLASSIC_XCBUI_H_
|
||||
#define _FCITX_UI_CLASSIC_XCBUI_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <cairo.h>
|
||||
#include <pango/pango-types.h>
|
||||
#include <pango/pangocairo.h>
|
||||
#include <xcb/xcb.h>
|
||||
#include <xcb/xcb_ewmh.h>
|
||||
#include <xcb/xproto.h>
|
||||
#include "fcitx-utils/eventloopinterface.h"
|
||||
#include "fcitx-utils/handlertable_details.h"
|
||||
#include "fcitx-utils/misc.h"
|
||||
#include "fcitx-utils/rect.h"
|
||||
#include "fcitx/userinterface.h"
|
||||
#include "classicui.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
class XCBInputWindow;
|
||||
class XCBTrayWindow;
|
||||
|
||||
enum class MultiScreenExtension { Randr, Xinerama, EXTNone };
|
||||
|
||||
enum class XCBHintStyle { Default, NoHint, Medium, Slight, Full };
|
||||
|
||||
enum class XCBRGBA { Default, NoRGBA, RGB, BGR, VRGB, VBGR };
|
||||
|
||||
class XCBWindow;
|
||||
|
||||
struct XCBFontOption {
|
||||
int dpi = -1;
|
||||
bool antialias = true;
|
||||
XCBHintStyle hint = XCBHintStyle::Default;
|
||||
XCBRGBA rgba = XCBRGBA::Default;
|
||||
|
||||
void setupPangoContext(PangoContext *context) const;
|
||||
};
|
||||
|
||||
class XCBUI : public UIInterface {
|
||||
public:
|
||||
XCBUI(M3UI *parent, const std::string &name, xcb_connection_t *conn,
|
||||
int defaultScreen);
|
||||
~XCBUI();
|
||||
|
||||
M3UI *parent() const { return parent_; }
|
||||
const std::string &displayName() const { return displayName_; }
|
||||
xcb_connection_t *connection() const { return conn_; }
|
||||
xcb_ewmh_connection_t *ewmh() const { return ewmh_; }
|
||||
xcb_window_t root() const { return root_; }
|
||||
int defaultScreen() const { return defaultScreen_; }
|
||||
xcb_colormap_t colorMap() const { return colorMap_; }
|
||||
xcb_visualid_t visualId() const;
|
||||
void update(UserInterfaceComponent component,
|
||||
InputContext *inputContext) override;
|
||||
void updateCursor(InputContext *inputContext) override;
|
||||
void updateCurrentInputMethod(InputContext *inputContext) override;
|
||||
void suspend() override;
|
||||
void resume() override;
|
||||
void setEnableTray(bool enable) override;
|
||||
void setCairoDevice(cairo_device_t *device);
|
||||
|
||||
const auto &screenRects() { return rects_; }
|
||||
int dpiByPosition(int x, int y);
|
||||
int scaledDPI(int dpi);
|
||||
const XCBFontOption &fontOption() const { return fontOption_; }
|
||||
|
||||
bool grabPointer(XCBWindow *window);
|
||||
void ungrabPointer();
|
||||
XCBWindow *pointerGrabber() const { return pointerGrabber_; }
|
||||
|
||||
private:
|
||||
void refreshCompositeManager();
|
||||
void refreshManager();
|
||||
void readXSettings();
|
||||
void initScreen();
|
||||
void scheduleUpdateScreen();
|
||||
|
||||
static void destroyCairoDevice(cairo_device_t *device);
|
||||
|
||||
M3UI *parent_;
|
||||
UniqueCPtr<cairo_device_t, destroyCairoDevice> device_;
|
||||
std::string displayName_;
|
||||
xcb_connection_t *conn_;
|
||||
XCBWindow *pointerGrabber_ = nullptr;
|
||||
xcb_window_t root_ = XCB_WINDOW_NONE;
|
||||
xcb_ewmh_connection_t *ewmh_;
|
||||
int defaultScreen_;
|
||||
xcb_colormap_t colorMap_;
|
||||
bool needFreeColorMap_ = false;
|
||||
std::unique_ptr<XCBInputWindow> inputWindow_;
|
||||
std::unique_ptr<XCBTrayWindow> trayWindow_;
|
||||
|
||||
std::string iconThemeName_;
|
||||
|
||||
std::string compMgrAtomString_;
|
||||
xcb_atom_t compMgrAtom_ = XCB_ATOM_NONE;
|
||||
xcb_window_t compMgrWindow_ = XCB_WINDOW_NONE;
|
||||
|
||||
xcb_atom_t managerAtom_ = XCB_ATOM_NONE;
|
||||
xcb_atom_t xsettingsSelectionAtom_ = XCB_ATOM_NONE;
|
||||
xcb_atom_t xsettingsWindow_ = XCB_WINDOW_NONE;
|
||||
xcb_atom_t xsettingsAtom_ = XCB_ATOM_NONE;
|
||||
|
||||
XCBFontOption fontOption_;
|
||||
int maxDpi_ = -1;
|
||||
int primaryDpi_ = -1;
|
||||
int screenDpi_ = 96;
|
||||
MultiScreenExtension multiScreen_ = MultiScreenExtension::EXTNone;
|
||||
int xrandrFirstEvent_ = 0;
|
||||
std::unique_ptr<EventSourceTime> initScreenEvent_;
|
||||
|
||||
std::vector<std::pair<Rect, int>> rects_;
|
||||
|
||||
std::vector<std::unique_ptr<HandlerTableEntryBase>> eventHandlers_;
|
||||
};
|
||||
|
||||
void addEventMaskToWindow(xcb_connection_t *conn, xcb_window_t wid,
|
||||
uint32_t mask);
|
||||
} // namespace fcitx::m3ui
|
||||
|
||||
#endif // _FCITX_UI_CLASSIC_XCBUI_H_
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2016-2016 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
|
||||
#include "xcbwindow.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <cairo-xcb.h>
|
||||
#include <cairo.h>
|
||||
#include <xcb/xcb.h>
|
||||
#include <xcb/xcb_aux.h>
|
||||
#include <xcb/xproto.h>
|
||||
#include "fcitx-utils/misc.h"
|
||||
#include "fcitx-utils/rect.h"
|
||||
#include "common.h"
|
||||
#include "window.h"
|
||||
#include "xcb_public.h"
|
||||
#include "xcbui.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
XCBWindow::XCBWindow(XCBUI *ui, int width, int height) : ui_(ui) {
|
||||
Window::resize(width, height);
|
||||
}
|
||||
|
||||
XCBWindow::~XCBWindow() { destroyWindow(); }
|
||||
|
||||
void XCBWindow::createWindow(xcb_visualid_t vid, bool overrideRedirect) {
|
||||
auto *conn = ui_->connection();
|
||||
|
||||
if (wid_) {
|
||||
destroyWindow();
|
||||
}
|
||||
xcb_screen_t *screen = xcb_aux_get_screen(conn, ui_->defaultScreen());
|
||||
|
||||
xcb_colormap_t colorMap;
|
||||
CLASSICUI_DEBUG() << "Create window with vid: " << vid;
|
||||
if (vid == ui_->visualId()) {
|
||||
colorMap = ui_->colorMap();
|
||||
colorMapNeedFree_ = 0;
|
||||
CLASSICUI_DEBUG() << "Use shared color map: " << colorMap;
|
||||
} else if (vid) {
|
||||
colorMapNeedFree_ = xcb_generate_id(conn);
|
||||
xcb_create_colormap(conn, XCB_COLORMAP_ALLOC_NONE, colorMapNeedFree_,
|
||||
screen->root, vid);
|
||||
colorMap = colorMapNeedFree_;
|
||||
CLASSICUI_DEBUG() << "Use new color map: " << colorMapNeedFree_;
|
||||
} else {
|
||||
colorMapNeedFree_ = 0;
|
||||
colorMap = XCB_COPY_FROM_PARENT;
|
||||
CLASSICUI_DEBUG() << "Use color map copy from parent";
|
||||
}
|
||||
|
||||
wid_ = xcb_generate_id(conn);
|
||||
|
||||
auto depth = xcb_aux_get_depth_of_visual(screen, vid);
|
||||
|
||||
uint32_t valueMask = XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXEL |
|
||||
XCB_CW_BIT_GRAVITY | XCB_CW_BACKING_STORE |
|
||||
XCB_CW_OVERRIDE_REDIRECT | XCB_CW_SAVE_UNDER |
|
||||
XCB_CW_COLORMAP;
|
||||
|
||||
if (overrideRedirect) {
|
||||
valueMask |= XCB_CW_OVERRIDE_REDIRECT;
|
||||
}
|
||||
|
||||
xcb_params_cw_t params;
|
||||
memset(¶ms, 0, sizeof(params));
|
||||
params.back_pixel = 0;
|
||||
params.border_pixel = 0;
|
||||
params.bit_gravity = XCB_GRAVITY_NORTH_WEST;
|
||||
params.backing_store = XCB_BACKING_STORE_WHEN_MAPPED;
|
||||
params.override_redirect = overrideRedirect ? 1 : 0;
|
||||
params.save_under = 1;
|
||||
params.colormap = colorMap;
|
||||
vid_ = vid;
|
||||
|
||||
physicalWidth_ = physicalSizeFromLogical(width_);
|
||||
physicalHeight_ = physicalSizeFromLogical(height_);
|
||||
|
||||
auto cookie = xcb_aux_create_window_checked(
|
||||
conn, depth, wid_, screen->root, 0, 0, physicalWidth_, physicalHeight_,
|
||||
0, XCB_WINDOW_CLASS_INPUT_OUTPUT, vid, valueMask, ¶ms);
|
||||
if (auto error = makeUniqueCPtr(xcb_request_check(conn, cookie))) {
|
||||
CLASSICUI_DEBUG() << "Create window failed: "
|
||||
<< static_cast<int>(error->error_code) << " " << vid
|
||||
<< " " << colorMap;
|
||||
} else {
|
||||
CLASSICUI_DEBUG() << "Window created id: " << wid_;
|
||||
}
|
||||
constexpr uint32_t XEMBED_VERSION = 0;
|
||||
constexpr uint32_t XEMBED_MAPPED = (1 << 0);
|
||||
uint32_t data[] = {XEMBED_VERSION, XEMBED_MAPPED};
|
||||
xcb_atom_t _XEMBED_INFO = ui_->parent()->xcb()->call<IXCBModule::atom>(
|
||||
ui_->displayName(), "_XEMBED_INFO", false);
|
||||
xcb_change_property(conn, XCB_PROP_MODE_REPLACE, wid_, _XEMBED_INFO,
|
||||
_XEMBED_INFO, 32, 2, data);
|
||||
|
||||
eventFilter_ = ui_->parent()->xcb()->call<IXCBModule::addEventFilter>(
|
||||
ui_->displayName(),
|
||||
[this](xcb_connection_t *, xcb_generic_event_t *event) {
|
||||
return filterEvent(event);
|
||||
});
|
||||
|
||||
surface_.reset(cairo_xcb_surface_create(
|
||||
conn, wid_,
|
||||
vid ? xcb_aux_find_visual_by_id(screen, vid)
|
||||
: xcb_aux_find_visual_by_id(screen, screen->root_visual),
|
||||
physicalWidth_, physicalHeight_));
|
||||
if (surface_) {
|
||||
cairo_surface_set_device_scale(surface_.get(), scale_, scale_);
|
||||
ui_->setCairoDevice(cairo_surface_get_device(surface_.get()));
|
||||
}
|
||||
contentSurface_.reset();
|
||||
|
||||
postCreateWindow();
|
||||
}
|
||||
|
||||
void XCBWindow::destroyWindow() {
|
||||
auto *conn = ui_->connection();
|
||||
eventFilter_.reset();
|
||||
if (wid_) {
|
||||
xcb_destroy_window(conn, wid_);
|
||||
wid_ = 0;
|
||||
}
|
||||
if (colorMapNeedFree_) {
|
||||
xcb_free_colormap(conn, colorMapNeedFree_);
|
||||
colorMapNeedFree_ = 0;
|
||||
}
|
||||
|
||||
if (ui_->pointerGrabber() == this) {
|
||||
ui_->ungrabPointer();
|
||||
}
|
||||
}
|
||||
|
||||
void XCBWindow::setScale(double scale) {
|
||||
if (scale_ != scale && scale > 0) {
|
||||
scale_ = scale;
|
||||
cairo_surface_set_device_scale(surface_.get(), scale_, scale_);
|
||||
}
|
||||
}
|
||||
|
||||
void XCBWindow::resize(unsigned int width, unsigned int height) {
|
||||
auto newPhysicalWidth = physicalSizeFromLogical(width);
|
||||
auto newPhysicalHeight = physicalSizeFromLogical(height);
|
||||
if (newPhysicalWidth != physicalWidth_ ||
|
||||
newPhysicalHeight != physicalHeight_) {
|
||||
const uint32_t vals[2] = {static_cast<uint32_t>(newPhysicalWidth),
|
||||
static_cast<uint32_t>(newPhysicalHeight)};
|
||||
xcb_configure_window(ui_->connection(), wid_,
|
||||
XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT,
|
||||
vals);
|
||||
cairo_xcb_surface_set_size(surface_.get(), newPhysicalWidth,
|
||||
newPhysicalHeight);
|
||||
physicalWidth_ = newPhysicalWidth;
|
||||
physicalHeight_ = newPhysicalHeight;
|
||||
}
|
||||
Window::resize(width, height);
|
||||
CLASSICUI_DEBUG() << "Resize: " << width << " " << height << " scale "
|
||||
<< scale_ << " physical " << physicalWidth_ << " "
|
||||
<< physicalHeight_;
|
||||
}
|
||||
|
||||
cairo_surface_t *XCBWindow::prerender() {
|
||||
#if 1
|
||||
contentSurface_.reset(cairo_surface_create_similar_image(
|
||||
surface_.get(), CAIRO_FORMAT_ARGB32, physicalWidth_, physicalHeight_));
|
||||
#else
|
||||
contentSurface_.reset(cairo_image_surface_create(
|
||||
CAIRO_FORMAT_ARGB32, physicalWidth_, physicalHeight_));
|
||||
#endif
|
||||
cairo_surface_set_device_scale(contentSurface_.get(), scale_, scale_);
|
||||
return contentSurface_.get();
|
||||
}
|
||||
|
||||
void XCBWindow::render() {
|
||||
auto *cr = cairo_create(surface_.get());
|
||||
cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
|
||||
cairo_set_source_surface(cr, contentSurface_.get(), 0, 0);
|
||||
cairo_paint(cr);
|
||||
cairo_destroy(cr);
|
||||
CLASSICUI_DEBUG() << "Render";
|
||||
}
|
||||
|
||||
double XCBWindow::logicalFromPhysical(double value) const {
|
||||
return std::floor(value / scale_);
|
||||
}
|
||||
|
||||
double XCBWindow::physicalFromLogical(double value) const {
|
||||
return std::round(value * scale_);
|
||||
}
|
||||
|
||||
int XCBWindow::physicalSizeFromLogical(double size) const {
|
||||
return std::max(1.0, physicalFromLogical(size));
|
||||
}
|
||||
|
||||
Rect XCBWindow::physicalFromLogical(const Rect &rect) const {
|
||||
return Rect()
|
||||
.setLeft(physicalFromLogical(rect.left()))
|
||||
.setTop(physicalFromLogical(rect.top()))
|
||||
.setRight(physicalFromLogical(rect.right()))
|
||||
.setBottom(physicalFromLogical(rect.bottom()));
|
||||
}
|
||||
|
||||
double XCBWindow::scaleForDPI(int dpi) {
|
||||
if (dpi <= 0) {
|
||||
return 1.0;
|
||||
}
|
||||
return static_cast<double>(dpi) / 96.0;
|
||||
}
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2016-2016 CSSlayer <wengxt@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*
|
||||
*/
|
||||
#ifndef _FCITX_UI_CLASSIC_XCBWINDOW_H_
|
||||
#define _FCITX_UI_CLASSIC_XCBWINDOW_H_
|
||||
|
||||
#include <memory>
|
||||
#include <cairo.h>
|
||||
#include <xcb/xcb.h>
|
||||
#include <xcb/xproto.h>
|
||||
#include "fcitx-utils/handlertable.h"
|
||||
#include "fcitx-utils/misc.h"
|
||||
#include "fcitx-utils/rect.h"
|
||||
#include "window.h"
|
||||
#include "xcb_public.h"
|
||||
#include "xcbui.h"
|
||||
|
||||
namespace fcitx::m3ui {
|
||||
|
||||
class XCBWindow : public Window {
|
||||
public:
|
||||
XCBWindow(XCBUI *ui, int width = 1, int height = 1);
|
||||
~XCBWindow();
|
||||
|
||||
void createWindow(xcb_visualid_t vid, bool overrideRedirect = true);
|
||||
virtual void postCreateWindow() {}
|
||||
void destroyWindow();
|
||||
void resize(unsigned int width, unsigned int height) override;
|
||||
|
||||
cairo_surface_t *prerender() override;
|
||||
void render() override;
|
||||
|
||||
virtual bool filterEvent(xcb_generic_event_t *event) = 0;
|
||||
|
||||
xcb_window_t wid() const { return wid_; }
|
||||
void setScale(double scale);
|
||||
|
||||
protected:
|
||||
double logicalFromPhysical(double value) const;
|
||||
double physicalFromLogical(double value) const;
|
||||
// X11 window dimensions must be at least one pixel.
|
||||
int physicalSizeFromLogical(double size) const;
|
||||
Rect physicalFromLogical(const Rect &rect) const;
|
||||
static double scaleForDPI(int dpi);
|
||||
|
||||
XCBUI *ui_;
|
||||
xcb_window_t wid_ = 0;
|
||||
xcb_colormap_t colorMapNeedFree_ = 0;
|
||||
xcb_visualid_t vid_ = 0;
|
||||
std::unique_ptr<HandlerTableEntry<XCBEventFilter>> eventFilter_;
|
||||
UniqueCPtr<cairo_surface_t, cairo_surface_destroy> surface_;
|
||||
UniqueCPtr<cairo_surface_t, cairo_surface_destroy> contentSurface_;
|
||||
int physicalWidth_ = 1;
|
||||
int physicalHeight_ = 1;
|
||||
double scale_ = 1.0;
|
||||
};
|
||||
|
||||
} // namespace fcitx::m3ui
|
||||
|
||||
#endif // _FCITX_UI_CLASSIC_XCBWINDOW_H_
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m7 5 4 4-4 4" fill="none" stroke="#D5C2C6" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 213 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="32" height="32" rx="10" fill="#7B294A" fill-opacity="0.84"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 167 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m7 4.5 4.5 4.5L7 13.5" fill="none" stroke="#D5C2C6" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 222 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="36" rx="11" fill="#2F282A" fill-opacity="0.78"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 167 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 4.5 6.5 9l4.5 4.5" fill="none" stroke="#D5C2C6" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 222 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="9" cy="9" r="6" fill="none" stroke="#D5C2C6" stroke-width="1.5"/>
|
||||
<circle cx="9" cy="9" r="3" fill="#FFB0C9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 218 B |
@@ -0,0 +1,117 @@
|
||||
[Metadata]
|
||||
Name=Material 3 Dark (m3ui)
|
||||
Version=1
|
||||
Author=m3ui
|
||||
Description=Material 3 theme for the m3ui Fcitx5 user interface
|
||||
ScaleWithDPI=True
|
||||
|
||||
[InputPanel]
|
||||
SurfaceColor=#2F282AF5
|
||||
OutlineColor=#FFFFFF1F
|
||||
HighlightFillColor=#5E3A49FF
|
||||
PanelRadius=18
|
||||
ItemRadius=12
|
||||
NormalColor=#EBE0E1
|
||||
HighlightCandidateColor=#FFD9E2
|
||||
HighlightColor=#FFB0C9
|
||||
HighlightBackgroundColor=#5E3A49
|
||||
CandidateLabelColor=#A8969B
|
||||
HighlightCandidateLabelColor=#E6BDC9
|
||||
CandidateCommentColor=#D5C2C6
|
||||
HighlightCandidateCommentColor=#FFD9E2
|
||||
LabelTextSizeFactor=78
|
||||
CommentTextSizeFactor=78
|
||||
FullWidthHighlight=False
|
||||
PageButtonAlignment=Center
|
||||
|
||||
[InputPanel/ContentMargin]
|
||||
Left=3
|
||||
Right=3
|
||||
Top=3
|
||||
Bottom=3
|
||||
|
||||
[InputPanel/TextMargin]
|
||||
Left=8
|
||||
Right=8
|
||||
Top=5
|
||||
Bottom=5
|
||||
|
||||
[InputPanel/Background]
|
||||
Image=panel.svg
|
||||
|
||||
[InputPanel/Background/Margin]
|
||||
Left=12
|
||||
Right=12
|
||||
Top=12
|
||||
Bottom=12
|
||||
|
||||
[InputPanel/Highlight]
|
||||
Image=highlight.svg
|
||||
|
||||
[InputPanel/Highlight/Margin]
|
||||
Left=10
|
||||
Right=10
|
||||
Top=10
|
||||
Bottom=10
|
||||
|
||||
[InputPanel/PrevPage]
|
||||
Image=prev.svg
|
||||
|
||||
[InputPanel/PrevPage/ClickMargin]
|
||||
Left=4
|
||||
Right=4
|
||||
Top=4
|
||||
Bottom=4
|
||||
|
||||
[InputPanel/NextPage]
|
||||
Image=next.svg
|
||||
|
||||
[InputPanel/NextPage/ClickMargin]
|
||||
Left=4
|
||||
Right=4
|
||||
Top=4
|
||||
Bottom=4
|
||||
|
||||
[Menu]
|
||||
NormalColor=#EBE0E1
|
||||
HighlightCandidateColor=#FFD9E2
|
||||
Spacing=2
|
||||
|
||||
[Menu/Background]
|
||||
Image=panel.svg
|
||||
|
||||
[Menu/Background/Margin]
|
||||
Left=12
|
||||
Right=12
|
||||
Top=12
|
||||
Bottom=12
|
||||
|
||||
[Menu/ContentMargin]
|
||||
Left=4
|
||||
Right=4
|
||||
Top=4
|
||||
Bottom=4
|
||||
|
||||
[Menu/Highlight]
|
||||
Image=highlight.svg
|
||||
|
||||
[Menu/Highlight/Margin]
|
||||
Left=10
|
||||
Right=10
|
||||
Top=10
|
||||
Bottom=10
|
||||
|
||||
[Menu/Separator]
|
||||
Color=#514347
|
||||
|
||||
[Menu/CheckBox]
|
||||
Image=radio.svg
|
||||
|
||||
[Menu/SubMenu]
|
||||
Image=arrow.svg
|
||||
|
||||
[Menu/TextMargin]
|
||||
Left=8
|
||||
Right=8
|
||||
Top=5
|
||||
Bottom=5
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m7 5 4 4-4 4" fill="none" stroke="#D5C2C6" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 213 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="32" height="32" rx="10" fill="#7B294A" fill-opacity="0.84"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 167 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m7 4.5 4.5 4.5L7 13.5" fill="none" stroke="#D5C2C6" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 222 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="36" rx="11" fill="#2F282A" fill-opacity="0.78"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 167 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 4.5 6.5 9l4.5 4.5" fill="none" stroke="#D5C2C6" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 222 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="9" cy="9" r="6" fill="none" stroke="#D5C2C6" stroke-width="1.5"/>
|
||||
<circle cx="9" cy="9" r="3" fill="#FFB0C9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 218 B |
@@ -0,0 +1,117 @@
|
||||
[Metadata]
|
||||
Name=Material 3 Light (m3ui)
|
||||
Version=1
|
||||
Author=m3ui
|
||||
Description=Material 3 theme for the m3ui Fcitx5 user interface
|
||||
ScaleWithDPI=True
|
||||
|
||||
[InputPanel]
|
||||
SurfaceColor=#FFF8F7F5
|
||||
OutlineColor=#00000026
|
||||
HighlightFillColor=#FFD9E2FF
|
||||
PanelRadius=18
|
||||
ItemRadius=12
|
||||
NormalColor=#231A1D
|
||||
HighlightCandidateColor=#3E001D
|
||||
HighlightColor=#8B4A62
|
||||
HighlightBackgroundColor=#FFD9E2
|
||||
CandidateLabelColor=#85737A
|
||||
HighlightCandidateLabelColor=#6E3A4F
|
||||
CandidateCommentColor=#52434A
|
||||
HighlightCandidateCommentColor=#5A2C42
|
||||
LabelTextSizeFactor=78
|
||||
CommentTextSizeFactor=78
|
||||
FullWidthHighlight=False
|
||||
PageButtonAlignment=Center
|
||||
|
||||
[InputPanel/ContentMargin]
|
||||
Left=3
|
||||
Right=3
|
||||
Top=3
|
||||
Bottom=3
|
||||
|
||||
[InputPanel/TextMargin]
|
||||
Left=8
|
||||
Right=8
|
||||
Top=5
|
||||
Bottom=5
|
||||
|
||||
[InputPanel/Background]
|
||||
Image=panel.svg
|
||||
|
||||
[InputPanel/Background/Margin]
|
||||
Left=12
|
||||
Right=12
|
||||
Top=12
|
||||
Bottom=12
|
||||
|
||||
[InputPanel/Highlight]
|
||||
Image=highlight.svg
|
||||
|
||||
[InputPanel/Highlight/Margin]
|
||||
Left=10
|
||||
Right=10
|
||||
Top=10
|
||||
Bottom=10
|
||||
|
||||
[InputPanel/PrevPage]
|
||||
Image=prev.svg
|
||||
|
||||
[InputPanel/PrevPage/ClickMargin]
|
||||
Left=4
|
||||
Right=4
|
||||
Top=4
|
||||
Bottom=4
|
||||
|
||||
[InputPanel/NextPage]
|
||||
Image=next.svg
|
||||
|
||||
[InputPanel/NextPage/ClickMargin]
|
||||
Left=4
|
||||
Right=4
|
||||
Top=4
|
||||
Bottom=4
|
||||
|
||||
[Menu]
|
||||
NormalColor=#231A1D
|
||||
HighlightCandidateColor=#3E001D
|
||||
Spacing=2
|
||||
|
||||
[Menu/Background]
|
||||
Image=panel.svg
|
||||
|
||||
[Menu/Background/Margin]
|
||||
Left=12
|
||||
Right=12
|
||||
Top=12
|
||||
Bottom=12
|
||||
|
||||
[Menu/ContentMargin]
|
||||
Left=4
|
||||
Right=4
|
||||
Top=4
|
||||
Bottom=4
|
||||
|
||||
[Menu/Highlight]
|
||||
Image=highlight.svg
|
||||
|
||||
[Menu/Highlight/Margin]
|
||||
Left=10
|
||||
Right=10
|
||||
Top=10
|
||||
Bottom=10
|
||||
|
||||
[Menu/Separator]
|
||||
Color=#514347
|
||||
|
||||
[Menu/CheckBox]
|
||||
Image=radio.svg
|
||||
|
||||
[Menu/SubMenu]
|
||||
Image=arrow.svg
|
||||
|
||||
[Menu/TextMargin]
|
||||
Left=8
|
||||
Right=8
|
||||
Top=5
|
||||
Bottom=5
|
||||