Add Gnome extensions
Signed-off-by: TiagoRG <tiago.rgarcia@ua.pt>
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0"?>
|
||||
<application-state>
|
||||
<context id="">
|
||||
<application id="org.gnome.TextEditor.desktop" score="269.1875" last-seen="1697888699"/>
|
||||
<application id="gns3.desktop" score="148" last-seen="1697551737"/>
|
||||
<application id="libreoffice-impress.desktop" score="57" last-seen="1697448357"/>
|
||||
<application id="xdg-desktop-portal-gnome.desktop" score="0" last-seen="1697845177"/>
|
||||
<application id="com.mattjakeman.ExtensionManager.desktop" score="106.21875" last-seen="1697661140"/>
|
||||
<application id="firefox.desktop" score="23690.529296875" last-seen="1697893808"/>
|
||||
<application id="org.gnome.tweaks.desktop" score="24" last-seen="1697831894"/>
|
||||
<application id="bitwarden.desktop" score="176.5" last-seen="1697573921"/>
|
||||
<application id="libreoffice-startcenter.desktop" score="0" last-seen="1697704039"/>
|
||||
<application id="org.gnome.gitlab.somas.Apostrophe.desktop" score="1067.75" last-seen="1697584992"/>
|
||||
<application id="org.gnome.eog.desktop" score="66.4375" last-seen="1697832092"/>
|
||||
<application id="discord.desktop" score="7680.3046875" last-seen="1697892578"/>
|
||||
<application id="steam.desktop" score="10.5" last-seen="1697837947"/>
|
||||
<application id="org.gnome.Terminal.Preferences.desktop" score="2" last-seen="1697532261"/>
|
||||
<application id="jetbrains-toolbox.desktop" score="2" last-seen="1697641968"/>
|
||||
<application id="org.gnome.Console.desktop" score="85" last-seen="1697888733"/>
|
||||
<application id="org.gnome.font-viewer.desktop" score="10" last-seen="1697810889"/>
|
||||
<application id="org.gnome.Settings.desktop" score="252.9140625" last-seen="1697837908"/>
|
||||
<application id="org.gnome.gitg.desktop" score="0" last-seen="1697718363"/>
|
||||
<application id="org.gnome.Epiphany.desktop" score="7.5" last-seen="1697479145"/>
|
||||
<application id="org.gnome.Logs.desktop" score="9" last-seen="1697486197"/>
|
||||
<application id="org.gnome.Calculator.desktop" score="2" last-seen="1697654029"/>
|
||||
<application id="libreoffice-draw.desktop" score="0" last-seen="1697532146"/>
|
||||
<application id="libreoffice-writer.desktop" score="72.5" last-seen="1697445797"/>
|
||||
<application id="jetbrains-rider.desktop" score="231.75" last-seen="1697534169"/>
|
||||
<application id="org.gnome.Calendar.desktop" score="10" last-seen="1697479055"/>
|
||||
<application id="supertux2.desktop" score="525" last-seen="1697841014"/>
|
||||
<application id="org.gnome.Terminal.desktop" score="5940.375" last-seen="1697893810"/>
|
||||
<application id="org.gnome.Boxes.desktop" score="602.125" last-seen="1697642236"/>
|
||||
<application id="org.gnome.Extensions.desktop" score="83.25" last-seen="1697497711"/>
|
||||
<application id="org.gnome.Nautilus.desktop" score="233.5" last-seen="1697888840"/>
|
||||
<application id="gnome-system-monitor.desktop" score="22.1875" last-seen="1697497874"/>
|
||||
<application id="mars.desktop" score="877.1875" last-seen="1697562135"/>
|
||||
<application id="org.gnome.Evince.desktop" score="1117.1875" last-seen="1697732798"/>
|
||||
<application id="org.gnome.Totem.desktop" score="0" last-seen="1697712163"/>
|
||||
<application id="libreoffice-calc.desktop" score="27" last-seen="1697704040"/>
|
||||
<application id="com.belmoussaoui.Decoder.desktop" score="5" last-seen="1697575136"/>
|
||||
<application id="io.element.Element.desktop" score="970.78125" last-seen="1697888475"/>
|
||||
<application id="gimp.desktop" score="12" last-seen="1697718441"/>
|
||||
<application id="spotify-launcher.desktop" score="649.66015625" last-seen="1697893545"/>
|
||||
<application id="org.gnome.Aisleriot.desktop" score="96" last-seen="1697844697"/>
|
||||
</context>
|
||||
</application-state>
|
|
@ -0,0 +1,35 @@
|
|||
import St from 'gi://St';
|
||||
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
|
||||
|
||||
export default class ControlBlurExtension extends Extension {
|
||||
enable() {
|
||||
this._settings = this.getSettings();
|
||||
this._dialog = Main.screenShield._dialog;
|
||||
if (this._dialog)
|
||||
this._dialog._updateBackgroundEffects = this._myEffects();
|
||||
}
|
||||
|
||||
_myEffects() {
|
||||
const themeContext = St.ThemeContext.get_for_stage(global.stage);
|
||||
|
||||
for (const widget of this._dialog._backgroundGroup) {
|
||||
const effect = widget.get_effect('blur');
|
||||
|
||||
if (effect) {
|
||||
effect.set({
|
||||
brightness: this._settings.get_double('brightness'),
|
||||
sigma: this._settings.get_int('sigma') * themeContext.scale_factor,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// unlock-dialog is used in session-modes because this extension purpose is
|
||||
// to tweak blur effect on lock screen itself.
|
||||
disable() {
|
||||
this._dialog = null;
|
||||
this._settings = null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"_generated": "Generated by SweetTooth, do not edit",
|
||||
"description": "Control the Blur Effect On Lock Screen.",
|
||||
"name": "Control Blur Effect On Lock Screen",
|
||||
"session-modes": [
|
||||
"unlock-dialog"
|
||||
],
|
||||
"settings-schema": "org.gnome.shell.extensions.blur",
|
||||
"shell-version": [
|
||||
"45"
|
||||
],
|
||||
"url": "https://github.com/PRATAP-KUMAR/Control_Blur_Effect_On_Lock_Screen",
|
||||
"uuid": "ControlBlurEffectOnLockScreen@pratap.fastmail.fm",
|
||||
"version": 24
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
import Adw from 'gi://Adw';
|
||||
import Gtk from 'gi://Gtk';
|
||||
|
||||
import {ExtensionPreferences} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
|
||||
|
||||
export default class ControlBlurExtensionPreferences extends ExtensionPreferences {
|
||||
fillPreferencesWindow(window) {
|
||||
window._settings = this.getSettings();
|
||||
|
||||
const adjustBlur = () => {
|
||||
let hbox = new Gtk.Box({orientation: Gtk.Orientation.HORIZONTAL, margin_top: 5});
|
||||
let blurLabel = new Gtk.Label({label: 'Adjust Sigma', xalign: 0, hexpand: true});
|
||||
|
||||
this.blur_adjustment = new Gtk.Adjustment({
|
||||
lower: 0,
|
||||
'step-increment': 1,
|
||||
'page-increment': 10,
|
||||
upper: 999,
|
||||
});
|
||||
|
||||
this.blur_scale = new Gtk.Scale({
|
||||
hexpand: true,
|
||||
margin_start: 20,
|
||||
visible: true,
|
||||
'draw-value': true,
|
||||
'value-pos': 'left',
|
||||
'can-focus': true,
|
||||
digits: 0,
|
||||
adjustment: this.blur_adjustment,
|
||||
});
|
||||
|
||||
this.resetBlurButton = new Gtk.Button({margin_start: 5});
|
||||
this.resetBlurButton.set_label("Reset to Extensions's Default Value");
|
||||
this.resetBlurButton.connect('clicked', () => {
|
||||
window._settings.set_int('sigma', 1);
|
||||
this.blur_scale.set_value(window._settings.get_int('sigma'));
|
||||
});
|
||||
|
||||
this.blur_scale.set_value(window._settings.get_int('sigma'));
|
||||
this.blur_scale.connect('value-changed', entry => {
|
||||
window._settings.set_int('sigma', entry.get_value());
|
||||
});
|
||||
|
||||
hbox.append(blurLabel);
|
||||
hbox.append(this.blur_scale);
|
||||
hbox.append(this.resetBlurButton);
|
||||
|
||||
return hbox;
|
||||
};
|
||||
|
||||
const adjustBrightness = () => {
|
||||
let hbox = new Gtk.Box({orientation: Gtk.Orientation.HORIZONTAL, margin_top: 5});
|
||||
let brightnessLabel = new Gtk.Label({label: 'Adjust Brightness', xalign: 0, hexpand: true});
|
||||
|
||||
this.brightness_adjustment = new Gtk.Adjustment({
|
||||
lower: 0,
|
||||
'step-increment': 0.05,
|
||||
'page-increment': 0.1,
|
||||
upper: 1,
|
||||
});
|
||||
|
||||
this.brightness_scale = new Gtk.Scale({
|
||||
hexpand: true,
|
||||
margin_start: 20,
|
||||
visible: true,
|
||||
'draw-value': true,
|
||||
'value-pos': 'left',
|
||||
'can-focus': true,
|
||||
digits: 2,
|
||||
adjustment: this.brightness_adjustment,
|
||||
});
|
||||
|
||||
this.resetBrightnessButton = new Gtk.Button({margin_start: 5});
|
||||
this.resetBrightnessButton.set_label("Reset to Extensions's Default Value");
|
||||
this.resetBrightnessButton.connect('clicked', () => {
|
||||
window._settings.set_double('brightness', 0.65);
|
||||
this.brightness_scale.set_value(window._settings.get_double('brightness'));
|
||||
});
|
||||
|
||||
this.brightness_scale.set_value(window._settings.get_double('brightness'));
|
||||
this.brightness_scale.connect('value-changed', entry => {
|
||||
window._settings.set_double('brightness', entry.get_value());
|
||||
});
|
||||
|
||||
hbox.append(brightnessLabel);
|
||||
hbox.append(this.brightness_scale);
|
||||
hbox.append(this.resetBrightnessButton);
|
||||
|
||||
return hbox;
|
||||
};
|
||||
|
||||
const addBoldText = text => {
|
||||
let txt = new Gtk.Label({xalign: 0, margin_top: 20});
|
||||
txt.set_markup(`<b>${text}</b>`);
|
||||
txt.set_wrap(true);
|
||||
return txt;
|
||||
};
|
||||
|
||||
const page = new Adw.PreferencesPage();
|
||||
window.add(page);
|
||||
|
||||
const group = new Adw.PreferencesGroup();
|
||||
page.add(group);
|
||||
|
||||
group.add(adjustBlur());
|
||||
group.add(adjustBrightness());
|
||||
group.add(addBoldText(
|
||||
'Please note that when Blur Sigma Value is set to 0, the Brightness will be Maximum Irrespective of the value set above.'
|
||||
));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
<schemalist>
|
||||
<schema id="org.gnome.shell.extensions.blur" path="/org/gnome/shell/extensions/blur/">
|
||||
|
||||
<key name="brightness" type="d">
|
||||
<range min="0" max="1" />
|
||||
<default>0.65</default>
|
||||
<summary>Blur Brightness</summary>
|
||||
<description>Controls the Blur Brightness. Value is effective between 0 to 1. Please note that If the "Blur Sigma" value is set to '0', this value will be ignored and shows the lockscreen with max brightness.</description>
|
||||
</key>
|
||||
|
||||
<key name="sigma" type="i">
|
||||
<range min="0" max="999" />
|
||||
<default>1</default>
|
||||
<summary>Blur Sigma</summary>
|
||||
<description>Controls the Blur Sigma. Value is effective between 0 to 100 (But not limited) based on the Background Picture.
|
||||
Try some thing like 5, 10, 15, 20,... to suit your liking.</description>
|
||||
</key>
|
||||
|
||||
</schema>
|
||||
</schemalist>
|
|
@ -0,0 +1,5 @@
|
|||
/* uncomment below lines and edit the background-color as you wish if you want to change the password-prompt entry field on the lock screen */
|
||||
|
||||
/* .login-dialog-prompt-entry {
|
||||
background-color: #000;
|
||||
} */
|
|
@ -0,0 +1,339 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
|
||||
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.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, 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 or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
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 give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
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 Program or any portion
|
||||
of it, thus forming a work based on the Program, 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) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
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 Program, 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 Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) 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; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, 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 executable. However, as a
|
||||
special exception, the source code 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.
|
||||
|
||||
If distribution of executable or 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 counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program 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.
|
||||
|
||||
5. 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 Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program 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 to
|
||||
this License.
|
||||
|
||||
7. 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 Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program 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 Program.
|
||||
|
||||
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.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program 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.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the 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 Program
|
||||
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 Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, 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
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "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 PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. 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 PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), 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 Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. 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.
|
||||
|
||||
{description}
|
||||
Copyright (C) {year} {fullname}
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
{signature of Ty Coon}, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
|
@ -0,0 +1,577 @@
|
|||
import Clutter from 'gi://Clutter';
|
||||
import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
import St from 'gi://St'
|
||||
|
||||
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
|
||||
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
import * as Util from 'resource:///org/gnome/shell/misc/util.js';
|
||||
|
||||
import * as Sensors from './sensors.js';
|
||||
|
||||
import {Extension, gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js';
|
||||
|
||||
import * as MessageTray from 'resource:///org/gnome/shell/ui/messageTray.js';
|
||||
import * as Values from './values.js';
|
||||
import * as Config from 'resource:///org/gnome/shell/misc/config.js';
|
||||
import * as MenuItem from './menuItem.js';
|
||||
|
||||
let vitalsMenu;
|
||||
|
||||
var VitalsMenuButton = GObject.registerClass({
|
||||
GTypeName: 'VitalsMenuButton',
|
||||
}, class VitalsMenuButton extends PanelMenu.Button {
|
||||
_init(extensionObject) {
|
||||
super._init(Clutter.ActorAlign.FILL);
|
||||
|
||||
this._extensionObject = extensionObject;
|
||||
this._settings = extensionObject.getSettings();
|
||||
|
||||
this._sensorIcons = {
|
||||
'temperature' : { 'icon': 'temperature-symbolic.svg' },
|
||||
'voltage' : { 'icon': 'voltage-symbolic.svg' },
|
||||
'fan' : { 'icon': 'fan-symbolic.svg' },
|
||||
'memory' : { 'icon': 'memory-symbolic.svg' },
|
||||
'processor' : { 'icon': 'cpu-symbolic.svg' },
|
||||
'system' : { 'icon': 'system-symbolic.svg' },
|
||||
'network' : { 'icon': 'network-symbolic.svg',
|
||||
'icon-rx': 'network-download-symbolic.svg',
|
||||
'icon-tx': 'network-upload-symbolic.svg' },
|
||||
'storage' : { 'icon': 'storage-symbolic.svg' },
|
||||
'battery' : { 'icon': 'battery-symbolic.svg' }
|
||||
}
|
||||
|
||||
this._warnings = [];
|
||||
this._sensorMenuItems = {};
|
||||
this._hotLabels = {};
|
||||
this._hotIcons = {};
|
||||
this._groups = {};
|
||||
this._widths = {};
|
||||
this._last_query = new Date().getTime();
|
||||
|
||||
this._sensors = new Sensors.Sensors(this._settings, this._sensorIcons);
|
||||
this._values = new Values.Values(this._settings, this._sensorIcons);
|
||||
this._menuLayout = new St.BoxLayout({
|
||||
vertical: false,
|
||||
clip_to_allocation: true,
|
||||
x_align: Clutter.ActorAlign.START,
|
||||
y_align: Clutter.ActorAlign.CENTER,
|
||||
reactive: true,
|
||||
x_expand: true,
|
||||
pack_start: false
|
||||
});
|
||||
|
||||
this._drawMenu();
|
||||
this.add_actor(this._menuLayout);
|
||||
this._settingChangedSignals = [];
|
||||
this._refreshTimeoutId = null;
|
||||
|
||||
this._addSettingChangedSignal('update-time', this._updateTimeChanged.bind(this));
|
||||
this._addSettingChangedSignal('position-in-panel', this._positionInPanelChanged.bind(this));
|
||||
this._addSettingChangedSignal('menu-centered', this._positionInPanelChanged.bind(this));
|
||||
|
||||
let settings = [ 'use-higher-precision', 'alphabetize', 'hide-zeros', 'fixed-widths', 'hide-icons', 'unit', 'memory-measurement', 'include-public-ip', 'network-speed-format', 'storage-measurement', 'include-static-info' ];
|
||||
for (let setting of Object.values(settings))
|
||||
this._addSettingChangedSignal(setting, this._redrawMenu.bind(this));
|
||||
|
||||
// add signals for show- preference based categories
|
||||
for (let sensor in this._sensorIcons)
|
||||
this._addSettingChangedSignal('show-' + sensor, this._showHideSensorsChanged.bind(this));
|
||||
|
||||
this._initializeMenu();
|
||||
|
||||
// start off with fresh sensors
|
||||
this._querySensors();
|
||||
|
||||
// start monitoring sensors
|
||||
this._initializeTimer();
|
||||
}
|
||||
|
||||
_initializeMenu() {
|
||||
// display sensor categories
|
||||
for (let sensor in this._sensorIcons) {
|
||||
// groups associated sensors under accordion menu
|
||||
if (sensor in this._groups) continue;
|
||||
|
||||
this._groups[sensor] = new PopupMenu.PopupSubMenuMenuItem(_(this._ucFirst(sensor)), true);
|
||||
this._groups[sensor].icon.gicon = Gio.icon_new_for_string(this._extensionObject.path + '/icons/' + this._sensorIcons[sensor]['icon']);
|
||||
|
||||
// hide menu items that user has requested to not include
|
||||
if (!this._settings.get_boolean('show-' + sensor))
|
||||
this._groups[sensor].actor.hide();
|
||||
|
||||
if (!this._groups[sensor].status) {
|
||||
this._groups[sensor].status = this._defaultLabel();
|
||||
this._groups[sensor].actor.insert_child_at_index(this._groups[sensor].status, 4);
|
||||
this._groups[sensor].status.text = _('No Data');
|
||||
}
|
||||
|
||||
this.menu.addMenuItem(this._groups[sensor]);
|
||||
}
|
||||
|
||||
// add separator
|
||||
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
|
||||
|
||||
let item = new PopupMenu.PopupBaseMenuItem({
|
||||
reactive: false,
|
||||
style_class: 'vitals-menu-button-container'
|
||||
});
|
||||
|
||||
let customButtonBox = new St.BoxLayout({
|
||||
style_class: 'vitals-button-box',
|
||||
vertical: false,
|
||||
clip_to_allocation: true,
|
||||
x_align: Clutter.ActorAlign.CENTER,
|
||||
y_align: Clutter.ActorAlign.CENTER,
|
||||
reactive: true,
|
||||
x_expand: true,
|
||||
pack_start: false
|
||||
});
|
||||
|
||||
// custom round refresh button
|
||||
let refreshButton = this._createRoundButton('view-refresh-symbolic', _('Refresh'));
|
||||
refreshButton.connect('clicked', (self) => {
|
||||
// force refresh by clearing history
|
||||
this._sensors.resetHistory();
|
||||
this._values.resetHistory();
|
||||
|
||||
// make sure timer fires at next full interval
|
||||
this._updateTimeChanged();
|
||||
|
||||
// refresh sensors now
|
||||
this._querySensors();
|
||||
});
|
||||
customButtonBox.add_actor(refreshButton);
|
||||
|
||||
// custom round monitor button
|
||||
let monitorButton = this._createRoundButton('org.gnome.SystemMonitor-symbolic', _('System Monitor'));
|
||||
monitorButton.connect('clicked', (self) => {
|
||||
this.menu._getTopMenu().close();
|
||||
Util.spawn(this._settings.get_string('monitor-cmd').split(" "));
|
||||
});
|
||||
customButtonBox.add_actor(monitorButton);
|
||||
|
||||
// custom round preferences button
|
||||
let prefsButton = this._createRoundButton('preferences-system-symbolic', _('Preferences'));
|
||||
prefsButton.connect('clicked', (self) => {
|
||||
this.menu._getTopMenu().close();
|
||||
this._extensionObject.openPreferences();
|
||||
});
|
||||
customButtonBox.add_actor(prefsButton);
|
||||
|
||||
// now add the buttons to the top bar
|
||||
item.actor.add_actor(customButtonBox);
|
||||
|
||||
// add buttons
|
||||
this.menu.addMenuItem(item);
|
||||
|
||||
// query sensors on menu open
|
||||
this._menuStateChangeId = this.menu.connect('open-state-changed', (self, isMenuOpen) => {
|
||||
if (isMenuOpen) {
|
||||
// make sure timer fires at next full interval
|
||||
this._updateTimeChanged();
|
||||
|
||||
// refresh sensors now
|
||||
this._querySensors();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_createRoundButton(iconName) {
|
||||
let button = new St.Button({
|
||||
style_class: 'message-list-clear-button button vitals-button-action'
|
||||
});
|
||||
|
||||
button.child = new St.Icon({
|
||||
icon_name: iconName
|
||||
});
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
_removeMissingHotSensors(hotSensors) {
|
||||
for (let i = hotSensors.length - 1; i >= 0; i--) {
|
||||
let sensor = hotSensors[i];
|
||||
|
||||
// make sure default icon (if any) stays visible
|
||||
if (sensor == '_default_icon_') continue;
|
||||
|
||||
// removes sensors that are no longer available
|
||||
if (!this._sensorMenuItems[sensor]) {
|
||||
hotSensors.splice(i, 1);
|
||||
this._removeHotLabel(sensor);
|
||||
this._removeHotIcon(sensor);
|
||||
}
|
||||
}
|
||||
|
||||
return hotSensors;
|
||||
}
|
||||
|
||||
_saveHotSensors(hotSensors) {
|
||||
// removes any sensors that may not currently be available
|
||||
hotSensors = this._removeMissingHotSensors(hotSensors);
|
||||
|
||||
this._settings.set_strv('hot-sensors', hotSensors.filter(
|
||||
function(item, pos) {
|
||||
return hotSensors.indexOf(item) == pos;
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
_initializeTimer() {
|
||||
// used to query sensors and update display
|
||||
let update_time = this._settings.get_int('update-time');
|
||||
this._refreshTimeoutId = GLib.timeout_add_seconds(
|
||||
GLib.PRIORITY_DEFAULT,
|
||||
update_time,
|
||||
(self) => {
|
||||
// only update menu if we have hot sensors
|
||||
if (Object.values(this._hotLabels).length > 0)
|
||||
this._querySensors();
|
||||
// keep the timer running
|
||||
return GLib.SOURCE_CONTINUE;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_createHotItem(key, value) {
|
||||
let icon = this._defaultIcon(key);
|
||||
this._hotIcons[key] = icon;
|
||||
this._menuLayout.add_actor(icon)
|
||||
|
||||
// don't add a label when no sensors are in the panel
|
||||
if (key == '_default_icon_') return;
|
||||
|
||||
let label = new St.Label({
|
||||
style_class: 'vitals-panel-label',
|
||||
text: (value)?value:'\u2026', // ...
|
||||
y_expand: true,
|
||||
y_align: Clutter.ActorAlign.START
|
||||
});
|
||||
|
||||
// attempt to prevent ellipsizes
|
||||
label.get_clutter_text().ellipsize = 0;
|
||||
|
||||
// keep track of label for removal later
|
||||
this._hotLabels[key] = label;
|
||||
|
||||
// prevent "called on the widget" "which is not in the stage" errors by adding before width below
|
||||
this._menuLayout.add_actor(label);
|
||||
|
||||
// support for fixed widths #55, save label (text) width
|
||||
this._widths[key] = label.width;
|
||||
}
|
||||
|
||||
_showHideSensorsChanged(self, sensor) {
|
||||
this._sensors.resetHistory();
|
||||
this._groups[sensor.substr(5)].visible = this._settings.get_boolean(sensor);
|
||||
}
|
||||
|
||||
_positionInPanelChanged() {
|
||||
this.container.get_parent().remove_actor(this.container);
|
||||
let position = this._positionInPanel();
|
||||
|
||||
// allows easily addressable boxes
|
||||
let boxes = {
|
||||
left: Main.panel._leftBox,
|
||||
center: Main.panel._centerBox,
|
||||
right: Main.panel._rightBox
|
||||
};
|
||||
|
||||
// update position when changed from preferences
|
||||
boxes[position[0]].insert_child_at_index(this.container, position[1]);
|
||||
}
|
||||
|
||||
_removeHotLabel(key) {
|
||||
if (key in this._hotLabels) {
|
||||
let label = this._hotLabels[key];
|
||||
delete this._hotLabels[key];
|
||||
// make sure set_label is not called on non existent actor
|
||||
label.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
_removeHotLabels() {
|
||||
for (let key in this._hotLabels)
|
||||
this._removeHotLabel(key);
|
||||
}
|
||||
|
||||
_removeHotIcon(key) {
|
||||
if (key in this._hotIcons) {
|
||||
this._hotIcons[key].destroy();
|
||||
delete this._hotIcons[key];
|
||||
}
|
||||
}
|
||||
|
||||
_removeHotIcons() {
|
||||
for (let key in this._hotIcons)
|
||||
this._removeHotIcon(key);
|
||||
}
|
||||
|
||||
_redrawMenu() {
|
||||
this._removeHotIcons();
|
||||
this._removeHotLabels();
|
||||
|
||||
for (let key in this._sensorMenuItems) {
|
||||
if (key.includes('-group')) continue;
|
||||
this._sensorMenuItems[key].destroy();
|
||||
delete this._sensorMenuItems[key];
|
||||
}
|
||||
|
||||
this._drawMenu();
|
||||
this._sensors.resetHistory();
|
||||
this._values.resetHistory();
|
||||
this._querySensors();
|
||||
}
|
||||
|
||||
_drawMenu() {
|
||||
// grab list of selected menubar icons
|
||||
let hotSensors = this._settings.get_strv('hot-sensors');
|
||||
for (let key of Object.values(hotSensors)) {
|
||||
// fixes issue #225 which started when _max_ was moved to the end
|
||||
if (key == '__max_network-download__') key = '__network-rx_max__';
|
||||
if (key == '__max_network-upload__') key = '__network-tx_max__';
|
||||
|
||||
this._createHotItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
_destroyTimer() {
|
||||
// invalidate and reinitialize timer
|
||||
if (this._refreshTimeoutId != null) {
|
||||
GLib.Source.remove(this._refreshTimeoutId);
|
||||
this._refreshTimeoutId = null;
|
||||
}
|
||||
}
|
||||
|
||||
_updateTimeChanged() {
|
||||
this._destroyTimer();
|
||||
this._initializeTimer();
|
||||
}
|
||||
|
||||
_addSettingChangedSignal(key, callback) {
|
||||
this._settingChangedSignals.push(this._settings.connect('changed::' + key, callback));
|
||||
}
|
||||
|
||||
_updateDisplay(label, value, type, key) {
|
||||
// update sensor value in menubar
|
||||
if (this._hotLabels[key]) {
|
||||
this._hotLabels[key].set_text(value);
|
||||
|
||||
// support for fixed widths #55
|
||||
if (this._settings.get_boolean('fixed-widths')) {
|
||||
// grab text box width and see if new text is wider than old text
|
||||
let width2 = this._hotLabels[key].get_clutter_text().width;
|
||||
if (width2 > this._widths[key]) {
|
||||
this._hotLabels[key].set_width(width2);
|
||||
this._widths[key] = width2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// have we added this sensor before?
|
||||
let item = this._sensorMenuItems[key];
|
||||
if (item) {
|
||||
// update sensor value in the group
|
||||
item.value = value;
|
||||
} else if (type.includes('-group')) {
|
||||
// update text next to group header
|
||||
let group = type.split('-')[0];
|
||||
if (this._groups[group]) {
|
||||
this._groups[group].status.text = value;
|
||||
this._sensorMenuItems[type] = this._groups[group];
|
||||
}
|
||||
} else {
|
||||
// add item to group for the first time
|
||||
let sensor = { 'label': label, 'value': value, 'type': type }
|
||||
this._appendMenuItem(sensor, key);
|
||||
}
|
||||
}
|
||||
|
||||
_appendMenuItem(sensor, key) {
|
||||
let split = sensor.type.split('-');
|
||||
let type = split[0];
|
||||
let icon = (split.length == 2)?'icon-' + split[1]:'icon';
|
||||
let gicon = Gio.icon_new_for_string(this._extensionObject.path + '/icons/' + this._sensorIcons[type][icon]);
|
||||
|
||||
let item = new MenuItem.MenuItem(gicon, key, sensor.label, sensor.value, this._hotLabels[key]);
|
||||
item.connect('toggle', (self) => {
|
||||
let hotSensors = this._settings.get_strv('hot-sensors');
|
||||
|
||||
if (self.checked) {
|
||||
// add selected sensor to panel
|
||||
hotSensors.push(self.key);
|
||||
this._createHotItem(self.key, self.value);
|
||||
} else {
|
||||
// remove selected sensor from panel
|
||||
hotSensors.splice(hotSensors.indexOf(self.key), 1);
|
||||
this._removeHotLabel(self.key);
|
||||
this._removeHotIcon(self.key);
|
||||
}
|
||||
|
||||
if (hotSensors.length <= 0) {
|
||||
// add generic icon to panel when no sensors are selected
|
||||
hotSensors.push('_default_icon_');
|
||||
this._createHotItem('_default_icon_');
|
||||
} else {
|
||||
let defIconPos = hotSensors.indexOf('_default_icon_');
|
||||
if (defIconPos >= 0) {
|
||||
// remove generic icon from panel when sensors are selected
|
||||
hotSensors.splice(defIconPos, 1);
|
||||
this._removeHotIcon('_default_icon_');
|
||||
}
|
||||
}
|
||||
|
||||
// this code is called asynchronously - make sure to save it for next round
|
||||
this._saveHotSensors(hotSensors);
|
||||
});
|
||||
|
||||
this._sensorMenuItems[key] = item;
|
||||
let i = Object.keys(this._sensorMenuItems[key]).length;
|
||||
|
||||
// alphabetize the sensors for these categories
|
||||
if (this._settings.get_boolean('alphabetize')) {
|
||||
let menuItems = this._groups[type].menu._getMenuItems();
|
||||
for (i = 0; i < menuItems.length; i++)
|
||||
// use natural sort order for system load, etc
|
||||
if (menuItems[i].label.localeCompare(item.label, undefined, { numeric: true, sensitivity: 'base' }) > 0)
|
||||
break;
|
||||
}
|
||||
|
||||
this._groups[type].menu.addMenuItem(item, i);
|
||||
}
|
||||
|
||||
_defaultLabel() {
|
||||
return new St.Label({
|
||||
y_expand: true,
|
||||
y_align: Clutter.ActorAlign.CENTER
|
||||
});
|
||||
}
|
||||
|
||||
_defaultIcon(key) {
|
||||
let split = key.replaceAll('_', ' ').trim().split(' ')[0].split('-');
|
||||
let type = split[0];
|
||||
|
||||
let icon = new St.Icon({
|
||||
style_class: 'system-status-icon vitals-panel-icon-' + type,
|
||||
reactive: true
|
||||
});
|
||||
|
||||
// second condition prevents crash due to issue #225, which started when _max_ was moved to the end
|
||||
if (type == 'default' || !(type in this._sensorIcons)) {
|
||||
icon.gicon = Gio.icon_new_for_string(this._extensionObject.path + '/icons/' + this._sensorIcons['system']['icon']);
|
||||
} else if (!this._settings.get_boolean('hide-icons')) { // support for hide icons #80
|
||||
let iconObj = (split.length == 2)?'icon-' + split[1]:'icon';
|
||||
icon.gicon = Gio.icon_new_for_string(this._extensionObject.path + '/icons/' + this._sensorIcons[type][iconObj]);
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
|
||||
_ucFirst(string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
_positionInPanel() {
|
||||
let alignment = '';
|
||||
let gravity = 0;
|
||||
let arrow_pos = 0;
|
||||
|
||||
switch (this._settings.get_int('position-in-panel')) {
|
||||
case 0: // left
|
||||
alignment = 'left';
|
||||
gravity = -1;
|
||||
arrow_pos = 1;
|
||||
break;
|
||||
case 1: // center
|
||||
alignment = 'center';
|
||||
gravity = -1;
|
||||
arrow_pos = 0.5;
|
||||
break;
|
||||
case 2: // right
|
||||
alignment = 'right';
|
||||
gravity = 0;
|
||||
arrow_pos = 0;
|
||||
break;
|
||||
case 3: // far left
|
||||
alignment = 'left';
|
||||
gravity = 0;
|
||||
arrow_pos = 1;
|
||||
break;
|
||||
case 4: // far right
|
||||
alignment = 'right';
|
||||
gravity = -1;
|
||||
arrow_pos = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
let centered = this._settings.get_boolean('menu-centered')
|
||||
|
||||
if (centered) arrow_pos = 0.5;
|
||||
|
||||
// set arrow position when initializing and moving vitals
|
||||
this.menu._arrowAlignment = arrow_pos;
|
||||
|
||||
return [alignment, gravity];
|
||||
}
|
||||
|
||||
_querySensors() {
|
||||
// figure out last run time
|
||||
let now = new Date().getTime();
|
||||
let dwell = (now - this._last_query) / 1000;
|
||||
this._last_query = now;
|
||||
|
||||
this._sensors.query((label, value, type, format) => {
|
||||
let key = '_' + type.replace('-group', '') + '_' + label.replace(' ', '_').toLowerCase() + '_';
|
||||
|
||||
// if a sensor is disabled, gray it out
|
||||
if (key in this._sensorMenuItems) {
|
||||
this._sensorMenuItems[key].setSensitive((value!='disabled'));
|
||||
|
||||
// don't continue below, last known value is shown
|
||||
if (value == 'disabled') return;
|
||||
}
|
||||
|
||||
let items = this._values.returnIfDifferent(dwell, label, value, type, format, key);
|
||||
for (let item of Object.values(items))
|
||||
this._updateDisplay(_(item[0]), item[1], item[2], item[3]);
|
||||
}, dwell);
|
||||
|
||||
if (this._warnings.length > 0) {
|
||||
this._notify('Vitals', this._warnings.join("\n"), 'folder-symbolic');
|
||||
this._warnings = [];
|
||||
}
|
||||
}
|
||||
|
||||
_notify(msg, details, icon) {
|
||||
let source = new MessageTray.Source('MyApp Information', icon);
|
||||
Main.messageTray.add(source);
|
||||
let notification = new MessageTray.Notification(source, msg, details);
|
||||
notification.setTransient(true);
|
||||
source.notify(notification);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._destroyTimer();
|
||||
|
||||
for (let signal of Object.values(this._settingChangedSignals))
|
||||
this._settings.disconnect(signal);
|
||||
|
||||
super.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
export default class VitalsExtension extends Extension {
|
||||
enable() {
|
||||
vitalsMenu = new VitalsMenuButton(this);
|
||||
let position = vitalsMenu._positionInPanel();
|
||||
Main.panel.addToStatusArea('vitalsMenu', vitalsMenu, position[1], position[0]);
|
||||
}
|
||||
|
||||
disable() {
|
||||
vitalsMenu.destroy();
|
||||
vitalsMenu = null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib'
|
||||
|
||||
// convert Uint8Array into a literal string
|
||||
function convertUint8ArrayToString(contents) {
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
return decoder.decode(contents).trim();
|
||||
}
|
||||
|
||||
export function File(path) {
|
||||
if (path.indexOf('https://') == -1)
|
||||
this.file = Gio.File.new_for_path(path);
|
||||
else
|
||||
this.file = Gio.File.new_for_uri(path);
|
||||
}
|
||||
|
||||
File.prototype.read = function(delimiter = '', strip_header = false) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
this.file.load_contents_async(null, function(file, res) {
|
||||
try {
|
||||
// grab contents of file or website
|
||||
let contents = file.load_contents_finish(res)[1];
|
||||
|
||||
// convert contents to string
|
||||
contents = convertUint8ArrayToString(contents);
|
||||
|
||||
// split contents by delimiter if passed in
|
||||
if (delimiter) contents = contents.split(delimiter);
|
||||
|
||||
// optionally strip header when converting to a list
|
||||
if (strip_header) contents.shift();
|
||||
|
||||
// return results
|
||||
resolve(contents);
|
||||
} catch (e) {
|
||||
reject(e.message);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
reject(e.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
File.prototype.list = function() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let max_items = 125, results = [];
|
||||
|
||||
try {
|
||||
this.file.enumerate_children_async(Gio.FILE_ATTRIBUTE_STANDARD_NAME, Gio.FileQueryInfoFlags.NONE, GLib.PRIORITY_LOW, null, function(file, res) {
|
||||
try {
|
||||
let enumerator = file.enumerate_children_finish(res);
|
||||
|
||||
let callback = function(enumerator, res) {
|
||||
try {
|
||||
let files = enumerator.next_files_finish(res);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
results.push(files[i].get_attribute_as_string(Gio.FILE_ATTRIBUTE_STANDARD_NAME));
|
||||
}
|
||||
|
||||
if (files.length == 0) {
|
||||
enumerator.close_async(GLib.PRIORITY_LOW, null, function(){});
|
||||
resolve(results);
|
||||
} else {
|
||||
enumerator.next_files_async(max_items, GLib.PRIORITY_LOW, null, callback);
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
enumerator.next_files_async(max_items, GLib.PRIORITY_LOW, null, callback);
|
||||
} catch (e) {
|
||||
reject(e.message);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
reject(e.message);
|
||||
}
|
||||
});
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16" height="16.001" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="#474747">
|
||||
<path d="M5 5v2h6V5z" overflow="visible"/>
|
||||
<path d="M5.469 0c-.49 0-.797.216-1.032.456C4.202.696 4 1.012 4 1.486V2H2v14h12V2h-2v-.406l-.002-.028a1.616 1.616 0 0 0-.416-1.011c-.236-.28-.62-.585-1.2-.553L10.438 0zm.53 2h4v2h2v10H4V4h2z" color="#bebebe" font-family="sans-serif" font-weight="400" overflow="visible" style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;isolation:auto;mix-blend-mode:normal;shape-padding:0;text-decoration-color:#000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none" white-space="normal"/>
|
||||
<path d="m5 8v2h6v-2zm0 3v2h6v-2z" overflow="visible"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 909 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" ?><svg enable-background="new 0 0 52 52" id="Layer_1" version="1.1" viewBox="0 0 52 52" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M38.9799805,2H2v48h48V13.0200195L38.9799805,2z M25.0005493,6.0021362h3v2h-3V6.0021362z M25.0005493,10.0021362h3v2h-3 V10.0021362z M25.0005493,14.0021362h3v2h-3V14.0021362z M19.0005493,6.0021362h3v2h-3V6.0021362z M19.0005493,10.0021362h3v2h-3 V10.0021362z M19.0005493,14.0021362h3v2h-3V14.0021362z M10.0005493,46.0021362h-3v-2h3V46.0021362z M10.0005493,42.0021362h-3v-2 h3V42.0021362z M10.0005493,38.0021362h-3v-2h3V38.0021362z M10.0005493,34.0021362h-3v-2h3V34.0021362z M10.0005493,30.0021362h-3 v-2h3V30.0021362z M10.0005493,25.0021362h-3v-2h3V25.0021362z M10.0005493,21.0021362h-3v-2h3V21.0021362z M10.0005493,16.0021362 h-3v-2h3V16.0021362z M10.0005493,12.0021362h-3v-2h3V12.0021362z M10.0005493,8.0021362h-3v-2h3V8.0021362z M16.0005493,46.0021362 h-3v-2h3V46.0021362z M16.0005493,42.0021362h-3v-2h3V42.0021362z M16.0005493,38.0021362h-3v-2h3V38.0021362z M16.0005493,34.0021362h-3v-2h3V34.0021362z M16.0005493,30.0021362h-3v-2h3V30.0021362z M16.0005493,25.0021362h-3v-2h3 V25.0021362z M16.0005493,21.0021362h-3v-2h3V21.0021362z M16.0005493,16.0021362h-3v-2h3V16.0021362z M16.0005493,12.0021362h-3v-2 h3V12.0021362z M16.0005493,8.0021362h-3v-2h3V8.0021362z M19,19h14v14H19V19z M22.0005493,46.0021362h-3v-2h3V46.0021362z M22.0005493,42.0021362h-3v-2h3V42.0021362z M22.0005493,38.0021362h-3v-2h3V38.0021362z M28.0005493,46.0021362h-3v-2h3 V46.0021362z M28.0005493,42.0021362h-3v-2h3V42.0021362z M28.0005493,38.0021362h-3v-2h3V38.0021362z M34.0005493,46.0021362h-3v-2 h3V46.0021362z M34.0005493,42.0021362h-3v-2h3V42.0021362z M34.0005493,38.0021362h-3v-2h3V38.0021362z M34.0005493,16.0021362h-3 v-2h3V16.0021362z M34.0005493,12.0021362h-3v-2h3V12.0021362z M34.0005493,8.0021362h-3v-2h3V8.0021362z M40.0005493,46.0021362h-3 v-2h3V46.0021362z M40.0005493,42.0021362h-3v-2h3V42.0021362z M40.0005493,38.0021362h-3v-2h3V38.0021362z M40.0005493,34.0021362 h-3v-2h3V34.0021362z M40.0005493,30.0021362h-3v-2h3V30.0021362z M40.0005493,25.0021362h-3v-2h3V25.0021362z M40.0005493,21.0021362h-3v-2h3V21.0021362z M40.0005493,16.0021362h-3v-2h3V16.0021362z M40.0005493,12.0021362h-3v-2h3 V12.0021362z M46.0005493,46.0021362h-3v-2h3V46.0021362z M46.0005493,42.0021362h-3v-2h3V42.0021362z M46.0005493,38.0021362h-3v-2 h3V38.0021362z M46.0005493,34.0021362h-3v-2h3V34.0021362z M46.0005493,30.0021362h-3v-2h3V30.0021362z M46.0005493,25.0021362h-3 v-2h3V25.0021362z M46.0005493,21.0021362h-3v-2h3V21.0021362z M46.0005493,16.0021362h-3v-2h3V16.0021362z"/></svg>
|
After Width: | Height: | Size: 2.6 KiB |
|
@ -0,0 +1,133 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
id="svg7384"
|
||||
version="1.1"
|
||||
inkscape:version="0.48.3.1 r9886"
|
||||
height="16"
|
||||
sodipodi:docname="fan.svg"
|
||||
width="16">
|
||||
<metadata
|
||||
id="metadata90">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title>Gnome Symbolic Icon Theme</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
inkscape:object-paths="true"
|
||||
inkscape:cy="7.9523753"
|
||||
inkscape:current-layer="svg7384"
|
||||
inkscape:window-width="1680"
|
||||
pagecolor="#555753"
|
||||
showborder="false"
|
||||
showguides="true"
|
||||
inkscape:snap-nodes="false"
|
||||
objecttolerance="10"
|
||||
showgrid="true"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:window-x="0"
|
||||
inkscape:snap-bbox="true"
|
||||
bordercolor="#666666"
|
||||
id="namedview88"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-global="false"
|
||||
inkscape:window-y="27"
|
||||
gridtolerance="10"
|
||||
inkscape:zoom="45.254834"
|
||||
inkscape:window-height="991"
|
||||
borderopacity="1"
|
||||
guidetolerance="10"
|
||||
inkscape:snap-bbox-midpoints="false"
|
||||
inkscape:cx="6.4701314"
|
||||
inkscape:bbox-paths="false"
|
||||
inkscape:snap-grids="true"
|
||||
inkscape:pageopacity="1"
|
||||
inkscape:snap-to-guides="true">
|
||||
<inkscape:grid
|
||||
visible="true"
|
||||
spacingx="1px"
|
||||
type="xygrid"
|
||||
spacingy="1px"
|
||||
id="grid4866"
|
||||
empspacing="2"
|
||||
enabled="true"
|
||||
snapvisiblegridlinesonly="true" />
|
||||
</sodipodi:namedview>
|
||||
<title
|
||||
id="title9167">Gnome Symbolic Icon Theme</title>
|
||||
<defs
|
||||
id="defs7386" />
|
||||
<g
|
||||
transform="translate(-261,-277)"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer9"
|
||||
inkscape:label="status"
|
||||
style="display:inline" />
|
||||
<g
|
||||
transform="translate(-261,-277)"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer10"
|
||||
inkscape:label="devices" />
|
||||
<g
|
||||
transform="translate(-261,-277)"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer11"
|
||||
inkscape:label="apps" />
|
||||
<g
|
||||
transform="translate(-261,-277)"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer12"
|
||||
inkscape:label="actions" />
|
||||
<g
|
||||
transform="translate(-261,-277)"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer13"
|
||||
inkscape:label="places" />
|
||||
<g
|
||||
transform="translate(-261,-277)"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer14"
|
||||
inkscape:label="mimetypes" />
|
||||
<g
|
||||
transform="translate(-261,-277)"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer15"
|
||||
inkscape:label="emblems"
|
||||
style="display:inline" />
|
||||
<g
|
||||
transform="translate(-261,-277)"
|
||||
inkscape:groupmode="layer"
|
||||
id="g4953"
|
||||
inkscape:label="categories"
|
||||
style="display:inline" />
|
||||
<path
|
||||
style="fill:#bebebe;fill-opacity:1;stroke:none"
|
||||
d="M 8 0 C 7.7662783 -3.1322393e-17 7.5411335 0.011610998 7.3125 0.03125 C 5.2102943 1.4256475 5.5987411 4.0108346 6.90625 6.34375 C 7.2209706 6.1372503 7.5954602 6 8 6 C 8.0316752 6 8.062435 5.9985435 8.09375 6 C 8.7335699 3.3508722 11.235427 1.3679906 11.96875 1.0625 C 10.798244 0.39154001 9.4458996 -7.7509341e-16 8 0 z M 4 1.09375 C 2.5986515 1.9071018 1.4765974 3.1207274 0.78125 4.59375 C 0.9182133 7.1283275 3.3250439 8.0908827 6 8.125 C 5.9974202 8.0834163 6 8.0422335 6 8 C 6 7.6007368 6.1109237 7.2184502 6.3125 6.90625 C 4.3514736 5.0277432 3.8996391 1.8856213 4 1.09375 z M 13.15625 3.0625 C 11.454675 3.0097524 9.9536158 4.4003424 8.875 6.21875 C 9.2647725 6.4100014 9.5848814 6.7069783 9.78125 7.09375 C 12.408696 6.3344077 15.40554 7.5273725 16 8 C 16 6.3023731 15.464331 4.7324062 14.5625 3.4375 C 14.086383 3.1993071 13.609414 3.0765477 13.15625 3.0625 z M 10 7.875 C 10.00258 7.9165837 10 7.9577665 10 8 C 10 8.3992632 9.8890763 8.7815498 9.6875 9.09375 C 11.648527 10.972257 12.100361 14.114379 12 14.90625 C 13.401349 14.092898 14.523403 12.879273 15.21875 11.40625 C 15.081787 8.8716722 12.674956 7.9091173 10 7.875 z M 0 8 C -4.5501758e-16 9.6976269 0.53566854 11.267594 1.4375 12.5625 C 3.7013779 13.695079 5.7591272 12.083936 7.125 9.78125 C 6.7352275 9.5899986 6.4151186 9.2930217 6.21875 8.90625 C 3.5913037 9.6655923 0.5944604 8.4726275 0 8 z M 9.09375 9.65625 C 8.7790294 9.8627497 8.4045398 10 8 10 C 7.9683248 10 7.937565 10.001457 7.90625 10 C 7.26643 12.649128 4.7645729 14.632009 4.03125 14.9375 C 5.2017564 15.60846 6.5541004 16 8 16 C 8.2337217 16 8.4588665 15.988389 8.6875 15.96875 C 10.789706 14.574352 10.401259 11.989165 9.09375 9.65625 z "
|
||||
id="path3830" />
|
||||
<path
|
||||
sodipodi:type="arc"
|
||||
style="fill:#bebebe;fill-opacity:1;stroke:none"
|
||||
id="path3896"
|
||||
sodipodi:cx="8.0433397"
|
||||
sodipodi:cy="8.0008545"
|
||||
sodipodi:rx="0.9722718"
|
||||
sodipodi:ry="0.95017475"
|
||||
d="m 9.0156115,8.0008545 a 0.9722718,0.95017475 0 1 1 -1.9445436,0 0.9722718,0.95017475 0 1 1 1.9445436,0 z"
|
||||
transform="matrix(1.028519,0,0,1.052438,-0.27272757,-0.42040319)" />
|
||||
</svg>
|
After Width: | Height: | Size: 5.4 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" ?><svg enable-background="new 0 0 52 52" id="Layer_1" version="1.1" viewBox="0 0 52 52" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M50,41.0999756V11.4000244h-6.9978638V7.7766113C44.164978,7.364502,45,6.2706909,45,4.9799805 C45,3.3400269,43.6600342,2,42,2c-1.6499634,0-3,1.3400269-3,2.9799805c0,1.2908936,0.8413696,2.3848267,2.0021362,2.796814v3.62323 h-8V7.7766113C34.164978,7.364502,35,6.2706909,35,4.9799805C35,3.3400269,33.6600342,2,32,2c-1.6499634,0-3,1.3400269-3,2.9799805 c0,1.2908936,0.8413696,2.3848267,2.0021362,2.796814v3.62323h-8V7.7766113C24.164978,7.364502,25,6.2706909,25,4.9799805 C25,3.3400269,23.6600342,2,22,2c-1.6499634,0-3,1.3400269-3,2.9799805c0,1.2908936,0.8413696,2.3848267,2.0021362,2.796814v3.62323 h-8V7.7766113C14.164978,7.364502,15,6.2706909,15,4.9799805C15,3.3400269,13.6600342,2,12,2c-1.6499634,0-3,1.3400269-3,2.9799805 c0,1.2908936,0.8413696,2.3848267,2.0021362,2.796814v3.62323H2v29.6999512h9.0021362v3.1321411 C9.8413696,44.6417847,9,45.7312622,9,47.0299683C9,48.6699829,10.3500366,50,12,50c1.6600342,0,3-1.3300171,3-2.9700317 c0-1.298584-0.835022-2.3878784-1.9978638-2.7977295v-3.1322632h8v3.1321411C19.8413696,44.6417847,19,45.7312622,19,47.0299683 C19,48.6699829,20.3500366,50,22,50c1.6600342,0,3-1.3300171,3-2.9700317c0-1.298584-0.835022-2.3878784-1.9978638-2.7977295 v-3.1322632h8v3.1321411C29.8413696,44.6417847,29,45.7312622,29,47.0299683C29,48.6699829,30.3500366,50,32,50 c1.6600342,0,3-1.3300171,3-2.9700317c0-1.298584-0.835022-2.3878784-1.9978638-2.7977295v-3.1322632h8v3.1321411 C39.8413696,44.6417847,39,45.7312622,39,47.0299683C39,48.6699829,40.3500366,50,42,50c1.6600342,0,3-1.3300171,3-2.9700317 c0-1.298584-0.835022-2.3878784-1.9978638-2.7977295v-3.1322632H50z M7,16.3500366h38v19.7999878H7V16.3500366z"/></svg>
|
After Width: | Height: | Size: 1.8 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" ?><svg height="16px" version="1.1" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" xmlns:xlink="http://www.w3.org/1999/xlink"><title/><defs/><g fill="none" fill-rule="evenodd" id="Icons with numbers" stroke="none" stroke-width="1"><g fill="#000000" id="Group" transform="translate(-672.000000, -288.000000)"><path d="M679,293 L679,296 L677,296 L680,300 L683,296 L681,296 L681,293 Z M672,298 C672,296.651721 672.895887,295.507545 674.127761,295.131093 C674.500314,293.897932 675.645295,293 677,293 C677.174013,293 677.344566,293.014816 677.510466,293.043254 C678.195719,291.823839 679.50165,291 681,291 C683.209139,291 685,292.790861 685,295 C686.668415,295.005076 688,296.346276 688,298 C688,299.653483 686.652611,301 684.990522,301 L675.009478,301 C673.336631,301 672,299.656854 672,298 Z M672,298" id="Rectangle 169 copy 2"/></g></g></svg>
|
After Width: | Height: | Size: 936 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" ?><svg height="16px" version="1.1" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" xmlns:xlink="http://www.w3.org/1999/xlink"><title/><defs/><g fill="none" fill-rule="evenodd" id="Icons with numbers" stroke="none" stroke-width="1"><g fill="#000000" id="Group" transform="translate(-576.000000, -288.000000)"><path d="M576,298 C576,296.651721 576.895887,295.507545 578.127761,295.131093 C578.500314,293.897932 579.645295,293 581,293 C581.174013,293 581.344566,293.014816 581.510466,293.043254 C582.195719,291.823839 583.50165,291 585,291 C587.209139,291 589,292.790861 589,295 C590.668415,295.005076 592,296.346276 592,298 C592,299.653483 590.652611,301 588.990522,301 L579.009478,301 C577.336631,301 576,299.656854 576,298 Z M576,298" id="Rectangle 169"/></g></g></svg>
|
After Width: | Height: | Size: 864 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" ?><svg height="16px" version="1.1" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" xmlns:xlink="http://www.w3.org/1999/xlink"><title/><defs/><g fill="none" fill-rule="evenodd" id="Icons with numbers" stroke="none" stroke-width="1"><g fill="#000000" id="Group" transform="translate(-624.000000, -288.000000)"><path d="M631,297 L631,300 L633,300 L633,297 L635,297 L632,293 L629,297 Z M624,298 C624,296.651721 624.895887,295.507545 626.127761,295.131093 C626.500314,293.897932 627.645295,293 629,293 C629.174013,293 629.344566,293.014816 629.510466,293.043254 C630.195719,291.823839 631.50165,291 633,291 C635.209139,291 637,292.790861 637,295 C638.668415,295.005076 640,296.346276 640,298 C640,299.653483 638.652611,301 636.990522,301 L627.009478,301 C625.336631,301 624,299.656854 624,298 Z M624,298" id="Rectangle 169 copy"/></g></g></svg>
|
After Width: | Height: | Size: 934 B |
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" version="1.1">
|
||||
<path style="fill:#bebebe" d="M 3,1 C 2,1 2,2 2,2 V 14 C 2,14 2,15 3,15 H 13 C 13,15 14,15 14,14 V 2 C 14,2 14,1 13,1 Z M 8,3 C 10.21,3 12,4.79 12,7 12,9.21 10.21,11 8,11 H 4 V 7 C 4,4.79 5.79,3 8,3 Z M 8,5 C 6.9,5 6,5.9 6,7 6,8.1 6.9,9 8,9 9.1,9 10,8.1 10,7 10,5.9 9.1,5 8,5 Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 367 B |
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" version="1.1">
|
||||
<path style="fill:#bebebe" d="M 15,1 C 16,1 16,2 16,2 V 12 C 16,12 16,13 15,13 H 1 C 1,13 0,13 0,12 V 2 C 0,2 0,1 1,1 Z M 14,3 H 2 V 11 H 14 Z M 11,14 V 15 H 5 V 14 C 5,13 6,13 6,13 H 10 C 10,13 11,13 11,14 Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 298 B |
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
enable-background="new -0.2 0 18 30"
|
||||
height="30"
|
||||
version="1.1"
|
||||
viewBox="-0.2 0 30 30"
|
||||
width="30"
|
||||
xml:space="preserve"
|
||||
id="svg6"
|
||||
sodipodi:docname="if_Vector-icons_49_1041647.svg"
|
||||
inkscape:version="0.92.3 (3ce5693, 2018-03-11)"><metadata
|
||||
id="metadata10"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1070"
|
||||
inkscape:window-height="638"
|
||||
id="namedview8"
|
||||
showgrid="false"
|
||||
inkscape:zoom="7.8666667"
|
||||
inkscape:cx="5.2520864"
|
||||
inkscape:cy="14.6"
|
||||
inkscape:window-x="330"
|
||||
inkscape:window-y="226"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg6" /><defs
|
||||
id="defs2" /><path
|
||||
d="M 14.6,29.8 C 9.8,29.8 6,25.9 6,21.2 6,18.1 7.6,15.4 10,13.9 V 4.5 C 10,2 12.1,0 14.5,0 16.9,0 19,2 19,4.5 v 9.2 c 2.5,1.5 4.2,4.3 4.2,7.4 0.1,4.8 -3.8,8.7 -8.6,8.7 z M 17.3,15.4 17.1,5 C 17.1,3.8 15.7,2.9 14.5,2.9 13.3,2.9 12,3.8 12,5 v 10.5 c -2.1,1 -3.6,3.2 -3.6,5.7 0,3.5 2.8,6.3 6.3,6.3 3.5,0 6.3,-2.8 6.3,-6.3 0,-2.6 -1.5,-4.8 -3.7,-5.8 z m -2.7,10.5 c -2.6,0 -4.7,-2.1 -4.7,-4.7 0,-2 1.3,-3.7 3.1,-4.4 V 8 h 3 v 8.8 c 1.9,0.6 3.3,2.3 3.3,4.4 0,2.6 -2.1,4.7 -4.7,4.7 z"
|
||||
id="path4"
|
||||
inkscape:connector-curvature="0"
|
||||
style="clip-rule:evenodd;fill:#0d0d0d;fill-rule:evenodd" /></svg>
|
After Width: | Height: | Size: 2.1 KiB |
|
@ -0,0 +1,125 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
id="svg7384"
|
||||
version="1.1"
|
||||
inkscape:version="0.48.3.1 r9886"
|
||||
height="16"
|
||||
sodipodi:docname="voltage.svg"
|
||||
width="16">
|
||||
<metadata
|
||||
id="metadata90">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title>Gnome Symbolic Icon Theme</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
inkscape:object-paths="true"
|
||||
inkscape:cy="8.2822539"
|
||||
inkscape:current-layer="svg7384"
|
||||
inkscape:window-width="1680"
|
||||
pagecolor="#555753"
|
||||
showborder="false"
|
||||
showguides="true"
|
||||
inkscape:snap-nodes="false"
|
||||
objecttolerance="10"
|
||||
showgrid="true"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:window-x="0"
|
||||
inkscape:snap-bbox="true"
|
||||
bordercolor="#666666"
|
||||
id="namedview88"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-global="false"
|
||||
inkscape:window-y="27"
|
||||
gridtolerance="10"
|
||||
inkscape:zoom="68.593502"
|
||||
inkscape:window-height="991"
|
||||
borderopacity="1"
|
||||
guidetolerance="10"
|
||||
inkscape:snap-bbox-midpoints="false"
|
||||
inkscape:cx="6.2925429"
|
||||
inkscape:bbox-paths="false"
|
||||
inkscape:snap-grids="true"
|
||||
inkscape:pageopacity="1"
|
||||
inkscape:snap-to-guides="true">
|
||||
<inkscape:grid
|
||||
visible="true"
|
||||
spacingx="1px"
|
||||
type="xygrid"
|
||||
spacingy="1px"
|
||||
id="grid4866"
|
||||
empspacing="2"
|
||||
enabled="true"
|
||||
snapvisiblegridlinesonly="true" />
|
||||
</sodipodi:namedview>
|
||||
<title
|
||||
id="title9167">Gnome Symbolic Icon Theme</title>
|
||||
<defs
|
||||
id="defs7386" />
|
||||
<g
|
||||
transform="translate(-261,-277)"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer9"
|
||||
inkscape:label="status"
|
||||
style="display:inline" />
|
||||
<g
|
||||
transform="translate(-261,-277)"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer10"
|
||||
inkscape:label="devices" />
|
||||
<g
|
||||
transform="translate(-261,-277)"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer11"
|
||||
inkscape:label="apps" />
|
||||
<g
|
||||
transform="translate(-261,-277)"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer12"
|
||||
inkscape:label="actions" />
|
||||
<g
|
||||
transform="translate(-261,-277)"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer13"
|
||||
inkscape:label="places" />
|
||||
<g
|
||||
transform="translate(-261,-277)"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer14"
|
||||
inkscape:label="mimetypes" />
|
||||
<g
|
||||
transform="translate(-261,-277)"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer15"
|
||||
inkscape:label="emblems"
|
||||
style="display:inline" />
|
||||
<g
|
||||
transform="translate(-261,-277)"
|
||||
inkscape:groupmode="layer"
|
||||
id="g4953"
|
||||
inkscape:label="categories"
|
||||
style="display:inline" />
|
||||
<path
|
||||
style="fill:#bebebe;fill-opacity:1;stroke:none"
|
||||
d="m 11.40625,1 c -0.455108,0.038189 -0.61564,0.1791748 -1.25,0.625 L 2.84375,7.34375 C 1.9959525,8.0221961 2,8.0144269 2,9 l 0,1 4,0 -3.1875,3.1875 C 1.98932,14.004249 2.00091,13.998722 2,15 l 0,1 1,0.03125 c 0.986227,-0.01956 0.997937,-0.03056 1.84375,-0.625 L 12.15625,9.6875 C 13.004048,9.0090539 13,8.9855731 13,8 L 13,7 9,7 12.1875,3.8125 C 13.01068,2.9957516 12.99909,3.0012778 13,2 l 0,-1 -1,0 c -0.246557,0.00489 -0.442047,-0.0127296 -0.59375,0 z"
|
||||
id="path3898"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="sccsccccccccsccccccs" />
|
||||
</svg>
|
After Width: | Height: | Size: 3.8 KiB |
|
@ -0,0 +1,77 @@
|
|||
import Clutter from 'gi://Clutter';
|
||||
import GObject from 'gi://GObject';
|
||||
import St from 'gi://St'
|
||||
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
|
||||
|
||||
export const MenuItem = GObject.registerClass({
|
||||
|
||||
Signals: {
|
||||
'toggle': { param_types: [Clutter.Event.$gtype] },
|
||||
},
|
||||
|
||||
}, class MenuItem extends PopupMenu.PopupBaseMenuItem {
|
||||
|
||||
_init(icon, key, label, value, checked) {
|
||||
super._init({ reactive: true });
|
||||
|
||||
this._checked = checked;
|
||||
this._updateOrnament();
|
||||
|
||||
this._key = key;
|
||||
this._gIcon = icon;
|
||||
|
||||
// add icon
|
||||
this.add(new St.Icon({ style_class: 'popup-menu-icon', gicon : this._gIcon }));
|
||||
|
||||
// add label
|
||||
this._labelActor = new St.Label({ text: label });
|
||||
this.add(this._labelActor);
|
||||
|
||||
// add value
|
||||
this._valueLabel = new St.Label({ text: value });
|
||||
this._valueLabel.set_x_align(Clutter.ActorAlign.END);
|
||||
this._valueLabel.set_x_expand(true);
|
||||
this._valueLabel.set_y_expand(true);
|
||||
this.add(this._valueLabel);
|
||||
|
||||
this.actor._delegate = this;
|
||||
}
|
||||
|
||||
get checked() {
|
||||
return this._checked;
|
||||
}
|
||||
|
||||
get key() {
|
||||
return this._key;
|
||||
}
|
||||
|
||||
get gicon() {
|
||||
return this._gIcon;
|
||||
}
|
||||
|
||||
set value(value) {
|
||||
this._valueLabel.text = value;
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this._valueLabel.text;
|
||||
}
|
||||
|
||||
// prevents menu from being closed
|
||||
activate(event) {
|
||||
this._checked = !this._checked;
|
||||
this._updateOrnament();
|
||||
this.emit('toggle', event);
|
||||
}
|
||||
|
||||
_updateOrnament() {
|
||||
if (this._checked)
|
||||
this.setOrnament(PopupMenu.Ornament.CHECK);
|
||||
else
|
||||
this.setOrnament(PopupMenu.Ornament.NONE);
|
||||
}
|
||||
|
||||
get label() {
|
||||
return this._labelActor.text;
|
||||
}
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"_generated": "Generated by SweetTooth, do not edit",
|
||||
"description": "A glimpse into your computer's temperature, voltage, fan speed, memory usage, processor load, system resources, network speed and storage stats. This is a one stop shop to monitor all of your vital sensors. Uses asynchronous polling to provide a smooth user experience. Feature requests or bugs? Please use GitHub.",
|
||||
"gettext-domain": "vitals",
|
||||
"name": "Vitals",
|
||||
"settings-schema": "org.gnome.shell.extensions.vitals",
|
||||
"shell-version": [
|
||||
"45"
|
||||
],
|
||||
"url": "https://github.com/corecoding/Vitals",
|
||||
"uuid": "Vitals@CoreCoding.com",
|
||||
"version": 63
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
import Adw from 'gi://Adw';
|
||||
import Gio from 'gi://Gio';
|
||||
import GObject from 'gi://GObject';
|
||||
import Gtk from 'gi://Gtk';
|
||||
import {ExtensionPreferences, gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
|
||||
|
||||
/*
|
||||
if (sensor == 'show-storage' && this._settings.get_boolean(sensor)) {
|
||||
|
||||
let val = true;
|
||||
|
||||
try {
|
||||
let GTop = imports.gi.GTop;
|
||||
} catch (e) {
|
||||
val = false;
|
||||
}
|
||||
|
||||
let now = new Date().getTime();
|
||||
this._notify("Vitals", "Please run sudo apt install gir1.2-gtop-2.0", 'folder-symbolic');
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
const Settings = new GObject.Class({
|
||||
Name: 'Vitals.Settings',
|
||||
|
||||
_init: function(extensionObject, params) {
|
||||
this._extensionObject = extensionObject
|
||||
this.parent(params);
|
||||
|
||||
this._settings = extensionObject.getSettings();
|
||||
|
||||
this.builder = new Gtk.Builder();
|
||||
this.builder.set_translation_domain(this._extensionObject.metadata['gettext-domain']);
|
||||
this.builder.add_from_file(this._extensionObject.path + '/prefs.ui');
|
||||
this.widget = this.builder.get_object('prefs-container');
|
||||
|
||||
this._bind_settings();
|
||||
},
|
||||
|
||||
// Bind the gtk window to the schema settings
|
||||
_bind_settings: function() {
|
||||
let widget;
|
||||
|
||||
// process sensor toggles
|
||||
let sensors = [ 'show-temperature', 'show-voltage', 'show-fan',
|
||||
'show-memory', 'show-processor', 'show-system',
|
||||
'show-network', 'show-storage', 'use-higher-precision',
|
||||
'alphabetize', 'hide-zeros', 'include-public-ip',
|
||||
'show-battery', 'fixed-widths', 'hide-icons',
|
||||
'menu-centered', 'include-static-info' ];
|
||||
|
||||
for (let key in sensors) {
|
||||
let sensor = sensors[key];
|
||||
|
||||
widget = this.builder.get_object(sensor);
|
||||
widget.set_active(this._settings.get_boolean(sensor));
|
||||
widget.connect('state-set', (_, val) => {
|
||||
this._settings.set_boolean(sensor, val);
|
||||
});
|
||||
}
|
||||
|
||||
// process individual drop down sensor preferences
|
||||
sensors = [ 'position-in-panel', 'unit', 'network-speed-format', 'memory-measurement', 'storage-measurement', 'battery-slot' ];
|
||||
for (let key in sensors) {
|
||||
let sensor = sensors[key];
|
||||
|
||||
widget = this.builder.get_object(sensor);
|
||||
widget.set_active(this._settings.get_int(sensor));
|
||||
widget.connect('changed', (widget) => {
|
||||
this._settings.set_int(sensor, widget.get_active());
|
||||
});
|
||||
}
|
||||
|
||||
this._settings.bind('update-time', this.builder.get_object('update-time'), 'value', Gio.SettingsBindFlags.DEFAULT);
|
||||
|
||||
// process individual text entry sensor preferences
|
||||
sensors = [ 'storage-path', 'monitor-cmd' ];
|
||||
for (let key in sensors) {
|
||||
let sensor = sensors[key];
|
||||
|
||||
widget = this.builder.get_object(sensor);
|
||||
widget.set_text(this._settings.get_string(sensor));
|
||||
|
||||
widget.connect('changed', (widget) => {
|
||||
let text = widget.get_text();
|
||||
if (!text) text = widget.get_placeholder_text();
|
||||
this._settings.set_string(sensor, text);
|
||||
});
|
||||
}
|
||||
|
||||
// makes individual sensor preference boxes appear
|
||||
sensors = [ 'temperature', 'network', 'storage', 'memory', 'battery', 'system', 'processor' ];
|
||||
for (let key in sensors) {
|
||||
let sensor = sensors[key];
|
||||
|
||||
// create dialog for intelligent autohide advanced settings
|
||||
this.builder.get_object(sensor + '-prefs').connect('clicked', () => {
|
||||
let transientObj = this.widget.get_root();
|
||||
let title = sensor.charAt(0).toUpperCase() + sensor.slice(1);
|
||||
let dialog = new Gtk.Dialog({ title: _(title) + ' ' + _('Preferences'),
|
||||
transient_for: transientObj,
|
||||
use_header_bar: false,
|
||||
modal: true });
|
||||
|
||||
let box = this.builder.get_object(sensor + '_prefs');
|
||||
dialog.get_content_area().append(box);
|
||||
dialog.connect('response', (dialog, id) => {
|
||||
// remove the settings box so it doesn't get destroyed;
|
||||
dialog.get_content_area().remove(box);
|
||||
dialog.destroy();
|
||||
return;
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export default class VitalsPrefs extends ExtensionPreferences {
|
||||
fillPreferencesWindow(window) {
|
||||
window._settings = this.getSettings();
|
||||
|
||||
let settings = new Settings(this);
|
||||
let widget = settings.widget;
|
||||
|
||||
const page = new Adw.PreferencesPage();
|
||||
const group = new Adw.PreferencesGroup({});
|
||||
group.add(widget);
|
||||
page.add(group);
|
||||
window.add(page);
|
||||
window.set_default_size(widget.width, widget.height);
|
||||
widget.show();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<schemalist gettext-domain="gnome-shell-extensions">
|
||||
<schema id="org.gnome.shell.extensions.vitals" path="/org/gnome/shell/extensions/vitals/">
|
||||
<key name="hot-sensors" type="as">
|
||||
<default>['_memory_usage_', '_system_load_1m_', '__network-rx_max__']</default>
|
||||
<summary>Sensors to show in panel</summary>
|
||||
<description>List of sensors to be shown in the panel</description>
|
||||
</key>
|
||||
<key type="i" name="update-time">
|
||||
<default>5</default>
|
||||
<summary>Seconds between updates</summary>
|
||||
<description>Delay between sensor polling</description>
|
||||
</key>
|
||||
<key type="i" name="position-in-panel">
|
||||
<default>2</default>
|
||||
<summary>Position in panel</summary>
|
||||
<description>Position in Panel ('left', 'center', 'right')</description>
|
||||
</key>
|
||||
<key type="b" name="use-higher-precision">
|
||||
<default>false</default>
|
||||
<summary>Use higher precision</summary>
|
||||
<description>Show one extra digit after decimal</description>
|
||||
</key>
|
||||
<key type="b" name="alphabetize">
|
||||
<default>true</default>
|
||||
<summary>Alphabetize sensors</summary>
|
||||
<description>Display sensors in alphabetical order</description>
|
||||
</key>
|
||||
<key type="b" name="hide-zeros">
|
||||
<default>false</default>
|
||||
<summary>Hide zero values</summary>
|
||||
<description>Hide data from sensors that are invalid</description>
|
||||
</key>
|
||||
<key type="b" name="show-temperature">
|
||||
<default>true</default>
|
||||
<summary>Monitor temperature</summary>
|
||||
<description>Display temperature of various components</description>
|
||||
</key>
|
||||
<key type="i" name="unit">
|
||||
<default>0</default>
|
||||
<summary>Temperature unit</summary>
|
||||
<description>The unit ('centigrade' or 'fahrenheit') the extension should display the temperature in</description>
|
||||
</key>
|
||||
<key type="b" name="show-voltage">
|
||||
<default>true</default>
|
||||
<summary>Monitor voltage</summary>
|
||||
<description>Display voltage of various components</description>
|
||||
</key>
|
||||
<key type="b" name="show-fan">
|
||||
<default>true</default>
|
||||
<summary>Monitor fan</summary>
|
||||
<description>Display fan rotation per minute</description>
|
||||
</key>
|
||||
<key type="b" name="show-memory">
|
||||
<default>true</default>
|
||||
<summary>Monitor memory</summary>
|
||||
<description>Display memory information</description>
|
||||
</key>
|
||||
<key type="b" name="show-processor">
|
||||
<default>true</default>
|
||||
<summary>Monitor processor</summary>
|
||||
<description>Display processor information</description>
|
||||
</key>
|
||||
<key type="b" name="show-system">
|
||||
<default>true</default>
|
||||
<summary>Monitor system</summary>
|
||||
<description>Display system information</description>
|
||||
</key>
|
||||
<key type="b" name="show-storage">
|
||||
<default>true</default>
|
||||
<summary>Monitor storage</summary>
|
||||
<description>Display storage information</description>
|
||||
</key>
|
||||
<key type="b" name="show-network">
|
||||
<default>true</default>
|
||||
<summary>Monitor network</summary>
|
||||
<description>Display network information</description>
|
||||
</key>
|
||||
<key type="b" name="include-public-ip">
|
||||
<default>true</default>
|
||||
<summary>Include public IP address</summary>
|
||||
<description>Display public IP address of internet connection</description>
|
||||
</key>
|
||||
<key type="i" name="network-speed-format">
|
||||
<default>0</default>
|
||||
<summary>Network speed format</summary>
|
||||
<description>Should speed display in bits or bytes?</description>
|
||||
</key>
|
||||
<key type="s" name="storage-path">
|
||||
<default>"/"</default>
|
||||
<summary>Storage path</summary>
|
||||
<description>Storage path for monitoring</description>
|
||||
</key>
|
||||
<key type="b" name="show-battery">
|
||||
<default>false</default>
|
||||
<summary>Monitor battery</summary>
|
||||
<description>Monitor battery health</description>
|
||||
</key>
|
||||
<key type="i" name="memory-measurement">
|
||||
<default>1</default>
|
||||
<summary>Memory measurement</summary>
|
||||
<description>Can use gigabyte or gibibyte for memory</description>
|
||||
</key>
|
||||
<key type="i" name="storage-measurement">
|
||||
<default>1</default>
|
||||
<summary>Storage measurement</summary>
|
||||
<description>Can use gigabyte or gibibyte for storage</description>
|
||||
</key>
|
||||
<key type="i" name="battery-slot">
|
||||
<default>0</default>
|
||||
<summary>Battery slot to monitor</summary>
|
||||
<description>Which numerical battery slot should vitals monitor</description>
|
||||
</key>
|
||||
<key type="b" name="fixed-widths">
|
||||
<default>true</default>
|
||||
<summary>Use fixed widths in top bar</summary>
|
||||
<description>Keep sensors in top bar from jumping around</description>
|
||||
</key>
|
||||
<key type="b" name="hide-icons">
|
||||
<default>false</default>
|
||||
<summary>Hide icons in top bar</summary>
|
||||
<description>Keep top bar clean by only showing sensor values</description>
|
||||
</key>
|
||||
<key type="b" name="menu-centered">
|
||||
<default>false</default>
|
||||
<summary>Make the menu centered</summary>
|
||||
<description>Center the menu to the icon regardless of the position in the panel</description>
|
||||
</key>
|
||||
<key type="s" name="monitor-cmd">
|
||||
<default>"gnome-system-monitor"</default>
|
||||
<summary>System Monitor command</summary>
|
||||
<description>The command run when system monitor button is clicked</description>
|
||||
</key>
|
||||
<key type="b" name="include-static-info">
|
||||
<default>false</default>
|
||||
<summary>Include processor static information</summary>
|
||||
<description>Display processor static information that doesn't change</description>
|
||||
</key>
|
||||
</schema>
|
||||
</schemalist>
|
|
@ -0,0 +1,671 @@
|
|||
/*
|
||||
Copyright (c) 2018, Chris Monahan <chris@corecoding.com>
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the GNOME nor the names of its contributors may be
|
||||
used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
import GObject from 'gi://GObject';
|
||||
import * as FileModule from './helpers/file.js';
|
||||
import { gettext as _ } from 'resource:///org/gnome/shell/extensions/extension.js';
|
||||
import NM from 'gi://NM';
|
||||
|
||||
let GTop, hasGTop = true;
|
||||
try {
|
||||
({default: GTop} = await import('gi://GTop'));
|
||||
} catch (err) {
|
||||
log(err);
|
||||
hasGTop = false;
|
||||
};
|
||||
|
||||
export const Sensors = GObject.registerClass({
|
||||
GTypeName: 'Sensors',
|
||||
}, class Sensors extends GObject.Object {
|
||||
_init(settings, sensorIcons) {
|
||||
this._settings = settings;
|
||||
this._sensorIcons = sensorIcons;
|
||||
|
||||
this.resetHistory();
|
||||
|
||||
this._last_processor = { 'core': {}, 'speed': [] };
|
||||
|
||||
if (hasGTop) {
|
||||
this.storage = new GTop.glibtop_fsusage();
|
||||
this._storageDevice = '';
|
||||
this._findStorageDevice();
|
||||
|
||||
this._lastRead = 0;
|
||||
this._lastWrite = 0;
|
||||
}
|
||||
}
|
||||
|
||||
_refreshIPAddress(callback) {
|
||||
// check IP address
|
||||
new FileModule.File('https://corecoding.com/vitals.php').read().then(contents => {
|
||||
let obj = JSON.parse(contents);
|
||||
this._returnValue(callback, 'Public IP', obj['IPv4'], 'network', 'string');
|
||||
}).catch(err => { });
|
||||
}
|
||||
|
||||
_findStorageDevice() {
|
||||
new FileModule.File('/proc/mounts').read("\n").then(lines => {
|
||||
for (let line of lines) {
|
||||
let loadArray = line.trim().split(/\s+/);
|
||||
if (loadArray[1] == this._settings.get_string('storage-path')) {
|
||||
this._storageDevice = loadArray[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}).catch(err => { });
|
||||
}
|
||||
|
||||
query(callback, dwell) {
|
||||
if (!this._hardware_detected) {
|
||||
// we could set _hardware_detected in discoverHardwareMonitors, but by
|
||||
// doing it here, we guarantee avoidance of race conditions
|
||||
this._hardware_detected = true;
|
||||
this._discoverHardwareMonitors(callback);
|
||||
}
|
||||
|
||||
for (let sensor in this._sensorIcons) {
|
||||
if (this._settings.get_boolean('show-' + sensor)) {
|
||||
if (sensor == 'temperature' || sensor == 'voltage' || sensor == 'fan') {
|
||||
// for temp, volt, fan, we have a shared handler
|
||||
this._queryTempVoltFan(callback, sensor);
|
||||
} else {
|
||||
// directly call queryFunction below
|
||||
let method = '_query' + sensor[0].toUpperCase() + sensor.slice(1);
|
||||
this[method](callback, dwell);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_queryTempVoltFan(callback, type) {
|
||||
for (let label in this._tempVoltFanSensors[type]) {
|
||||
let sensor = this._tempVoltFanSensors[type][label];
|
||||
|
||||
new FileModule.File(sensor['path']).read().then(value => {
|
||||
this._returnValue(callback, label, value, type, sensor['format']);
|
||||
}).catch(err => {
|
||||
this._returnValue(callback, label, 'disabled', type, sensor['format']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_queryMemory(callback) {
|
||||
// check memory info
|
||||
new FileModule.File('/proc/meminfo').read().then(lines => {
|
||||
let values = '', total = 0, avail = 0, swapTotal = 0, swapFree = 0, cached = 0, memFree = 0;
|
||||
|
||||
if (values = lines.match(/MemTotal:(\s+)(\d+) kB/)) total = values[2];
|
||||
if (values = lines.match(/MemAvailable:(\s+)(\d+) kB/)) avail = values[2];
|
||||
if (values = lines.match(/SwapTotal:(\s+)(\d+) kB/)) swapTotal = values[2];
|
||||
if (values = lines.match(/SwapFree:(\s+)(\d+) kB/)) swapFree = values[2];
|
||||
if (values = lines.match(/Cached:(\s+)(\d+) kB/)) cached = values[2];
|
||||
if (values = lines.match(/MemFree:(\s+)(\d+) kB/)) memFree = values[2];
|
||||
|
||||
let used = total - avail
|
||||
let utilized = used / total;
|
||||
|
||||
this._returnValue(callback, 'Usage', utilized, 'memory', 'percent');
|
||||
this._returnValue(callback, 'memory', utilized, 'memory-group', 'percent');
|
||||
this._returnValue(callback, 'Physical', total, 'memory', 'memory');
|
||||
this._returnValue(callback, 'Available', avail, 'memory', 'memory');
|
||||
this._returnValue(callback, 'Allocated', used, 'memory', 'memory');
|
||||
this._returnValue(callback, 'Cached', cached, 'memory', 'memory');
|
||||
this._returnValue(callback, 'Free', memFree, 'memory', 'memory');
|
||||
this._returnValue(callback, 'Swap', swapTotal - swapFree, 'memory', 'memory');
|
||||
}).catch(err => { });
|
||||
}
|
||||
|
||||
_queryProcessor(callback, dwell) {
|
||||
let columns = ['user', 'nice', 'system', 'idle', 'iowait', 'irq', 'softirq', 'steal', 'guest', 'guest_nice'];
|
||||
|
||||
// check processor usage
|
||||
new FileModule.File('/proc/stat').read("\n").then(lines => {
|
||||
let statistics = {};
|
||||
|
||||
for (let line of lines) {
|
||||
let reverse_data = line.match(/^(cpu\d*\s)(.+)/);
|
||||
if (reverse_data) {
|
||||
let cpu = reverse_data[1].trim();
|
||||
|
||||
if (!(cpu in statistics))
|
||||
statistics[cpu] = {};
|
||||
|
||||
if (!(cpu in this._last_processor['core']))
|
||||
this._last_processor['core'][cpu] = 0;
|
||||
|
||||
let stats = reverse_data[2].trim().split(' ').reverse();
|
||||
for (let column of columns)
|
||||
statistics[cpu][column] = parseInt(stats.pop());
|
||||
}
|
||||
}
|
||||
|
||||
let cores = Object.keys(statistics).length - 1;
|
||||
|
||||
for (let cpu in statistics) {
|
||||
let total = statistics[cpu]['user'] + statistics[cpu]['nice'] + statistics[cpu]['system'];
|
||||
|
||||
// make sure we have data to report
|
||||
if (this._last_processor['core'][cpu] > 0) {
|
||||
let delta = (total - this._last_processor['core'][cpu]) / dwell;
|
||||
|
||||
// /proc/stat provides overall usage for us under the 'cpu' heading
|
||||
if (cpu == 'cpu') {
|
||||
delta = delta / cores;
|
||||
this._returnValue(callback, 'processor', delta / 100, 'processor-group', 'percent');
|
||||
this._returnValue(callback, 'Usage', delta / 100, 'processor', 'percent');
|
||||
} else {
|
||||
this._returnValue(callback, _('Core %d').format(cpu.substr(3)), delta / 100, 'processor', 'percent');
|
||||
}
|
||||
}
|
||||
|
||||
this._last_processor['core'][cpu] = total;
|
||||
}
|
||||
|
||||
// if frequency scaling is enabled, gather cpu-freq values
|
||||
if (!this._processor_uses_cpu_info) {
|
||||
for (let core = 0; core <= cores; core++) {
|
||||
new FileModule.File('/sys/devices/system/cpu/cpu' + core + '/cpufreq/scaling_cur_freq').read().then(value => {
|
||||
this._last_processor['speed'][core] = parseInt(value);
|
||||
}).catch(err => { });
|
||||
}
|
||||
}
|
||||
}).catch(err => { });
|
||||
|
||||
// if frequency scaling is disabled, use cpuinfo for speed
|
||||
if (this._processor_uses_cpu_info) {
|
||||
// grab CPU frequency
|
||||
new FileModule.File('/proc/cpuinfo').read("\n").then(lines => {
|
||||
let freqs = [];
|
||||
for (let line of lines) {
|
||||
// grab megahertz
|
||||
let value = line.match(/^cpu MHz(\s+): ([+-]?\d+(\.\d+)?)/);
|
||||
if (value) freqs.push(parseFloat(value[2]));
|
||||
}
|
||||
|
||||
let sum = freqs.reduce((a, b) => a + b);
|
||||
let hertz = (sum / freqs.length) * 1000 * 1000;
|
||||
this._returnValue(callback, 'Frequency', hertz, 'processor', 'hertz');
|
||||
|
||||
//let max_hertz = Math.getMaxOfArray(freqs) * 1000 * 1000;
|
||||
//this._returnValue(callback, 'Boost', max_hertz, 'processor', 'hertz');
|
||||
}).catch(err => { });
|
||||
// if frequency scaling is enabled, cpu-freq reports
|
||||
} else if (Object.values(this._last_processor['speed']).length > 0) {
|
||||
let sum = this._last_processor['speed'].reduce((a, b) => a + b);
|
||||
let hertz = (sum / this._last_processor['speed'].length) * 1000;
|
||||
this._returnValue(callback, 'Frequency', hertz, 'processor', 'hertz');
|
||||
//let max_hertz = Math.getMaxOfArray(this._last_processor['speed']) * 1000;
|
||||
//this._returnValue(callback, 'Boost', max_hertz, 'processor', 'hertz');
|
||||
}
|
||||
}
|
||||
|
||||
_querySystem(callback) {
|
||||
// check load average
|
||||
new FileModule.File('/proc/sys/fs/file-nr').read("\t").then(loadArray => {
|
||||
this._returnValue(callback, 'Open Files', loadArray[0], 'system', 'string');
|
||||
}).catch(err => { });
|
||||
|
||||
// check load average
|
||||
new FileModule.File('/proc/loadavg').read(' ').then(loadArray => {
|
||||
let proc = loadArray[3].split('/');
|
||||
|
||||
this._returnValue(callback, 'Load 1m', loadArray[0], 'system', 'load');
|
||||
this._returnValue(callback, 'system', loadArray[0], 'system-group', 'load');
|
||||
this._returnValue(callback, 'Load 5m', loadArray[1], 'system', 'load');
|
||||
this._returnValue(callback, 'Load 15m', loadArray[2], 'system', 'load');
|
||||
this._returnValue(callback, 'Threads Active', proc[0], 'system', 'string');
|
||||
this._returnValue(callback, 'Threads Total', proc[1], 'system', 'string');
|
||||
}).catch(err => { });
|
||||
|
||||
// check uptime
|
||||
new FileModule.File('/proc/uptime').read(' ').then(upArray => {
|
||||
this._returnValue(callback, 'Uptime', upArray[0], 'system', 'uptime');
|
||||
|
||||
let cores = Object.keys(this._last_processor['core']).length - 1;
|
||||
if (cores > 0)
|
||||
this._returnValue(callback, 'Process Time', upArray[0] - upArray[1] / cores, 'processor', 'uptime');
|
||||
}).catch(err => { });
|
||||
}
|
||||
|
||||
_queryNetwork(callback, dwell) {
|
||||
// check network speed
|
||||
let directions = ['tx', 'rx'];
|
||||
let netbase = '/sys/class/net/';
|
||||
|
||||
new FileModule.File(netbase).list().then(interfaces => {
|
||||
for (let iface of interfaces) {
|
||||
for (let direction of directions) {
|
||||
// lo tx and rx are the same
|
||||
if (iface == 'lo' && direction == 'rx') continue;
|
||||
|
||||
new FileModule.File(netbase + iface + '/statistics/' + direction + '_bytes').read().then(value => {
|
||||
// issue #217 - don't include 'lo' traffic in Maximum calculations in values.js
|
||||
// by not using network-rx or network-tx
|
||||
let name = iface + ((iface == 'lo')?'':' ' + direction);
|
||||
|
||||
let type = 'network' + ((iface=='lo')?'':'-' + direction);
|
||||
this._returnValue(callback, name, value, type, 'storage');
|
||||
}).catch(err => { });
|
||||
}
|
||||
}
|
||||
}).catch(err => { });
|
||||
|
||||
// some may not want public ip checking
|
||||
if (this._settings.get_boolean('include-public-ip')) {
|
||||
// check the public ip every hour or when waking from sleep
|
||||
if (this._next_public_ip_check <= 0) {
|
||||
this._next_public_ip_check = 3600;
|
||||
|
||||
this._refreshIPAddress(callback);
|
||||
}
|
||||
|
||||
this._next_public_ip_check -= dwell;
|
||||
}
|
||||
|
||||
// wireless interface statistics
|
||||
new FileModule.File('/proc/net/wireless').read("\n", true).then(lines => {
|
||||
// wireless has two headers - first is stripped in helper function
|
||||
lines.shift();
|
||||
|
||||
// if multiple wireless device, we use the last one
|
||||
for (let line of lines) {
|
||||
let netArray = line.trim().split(/\s+/);
|
||||
let quality_pct = netArray[2].substr(0, netArray[2].length-1) / 70;
|
||||
let signal = netArray[3].substr(0, netArray[3].length-1);
|
||||
|
||||
this._returnValue(callback, 'WiFi Link Quality', quality_pct, 'network', 'percent');
|
||||
this._returnValue(callback, 'WiFi Signal Level', signal, 'network', 'string');
|
||||
}
|
||||
}).catch(err => { });
|
||||
}
|
||||
|
||||
_queryStorage(callback, dwell) {
|
||||
// display zfs arc status, if available
|
||||
new FileModule.File('/proc/spl/kstat/zfs/arcstats').read().then(lines => {
|
||||
let values = '', target = 0, maximum = 0, current = 0;
|
||||
|
||||
if (values = lines.match(/c(\s+)(\d+)(\s+)(\d+)/)) target = values[4];
|
||||
if (values = lines.match(/c_max(\s+)(\d+)(\s+)(\d+)/)) maximum = values[4];
|
||||
if (values = lines.match(/size(\s+)(\d+)(\s+)(\d+)/)) current = values[4];
|
||||
|
||||
// ZFS statistics
|
||||
this._returnValue(callback, 'ARC Target', target, 'storage', 'storage');
|
||||
this._returnValue(callback, 'ARC Maximum', maximum, 'storage', 'storage');
|
||||
this._returnValue(callback, 'ARC Current', current, 'storage', 'storage');
|
||||
}).catch(err => { });
|
||||
|
||||
// check disk performance stats
|
||||
new FileModule.File('/proc/diskstats').read("\n").then(lines => {
|
||||
for (let line of lines) {
|
||||
let loadArray = line.trim().split(/\s+/);
|
||||
if ('/dev/' + loadArray[2] == this._storageDevice) {
|
||||
var read = (loadArray[5] * 512);
|
||||
var write = (loadArray[9] * 512);
|
||||
this._returnValue(callback, 'Read total', read, 'storage', 'storage');
|
||||
this._returnValue(callback, 'Write total', write, 'storage', 'storage');
|
||||
this._returnValue(callback, 'Read rate', (read - this._lastRead) / dwell, 'storage', 'storage');
|
||||
this._returnValue(callback, 'Write rate', (write - this._lastWrite) / dwell, 'storage', 'storage');
|
||||
this._lastRead = read;
|
||||
this._lastWrite = write;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}).catch(err => { });
|
||||
|
||||
// skip rest of stats if gtop not available
|
||||
if (!hasGTop) return;
|
||||
|
||||
GTop.glibtop_get_fsusage(this.storage, this._settings.get_string('storage-path'));
|
||||
|
||||
let total = this.storage.blocks * this.storage.block_size;
|
||||
let avail = this.storage.bavail * this.storage.block_size;
|
||||
let free = this.storage.bfree * this.storage.block_size;
|
||||
let used = total - free;
|
||||
let reserved = (total - avail) - used;
|
||||
|
||||
this._returnValue(callback, 'Total', total, 'storage', 'storage');
|
||||
this._returnValue(callback, 'Used', used, 'storage', 'storage');
|
||||
this._returnValue(callback, 'Reserved', reserved, 'storage', 'storage');
|
||||
this._returnValue(callback, 'Free', avail, 'storage', 'storage');
|
||||
this._returnValue(callback, 'storage', avail, 'storage-group', 'storage');
|
||||
}
|
||||
|
||||
_queryBattery(callback) {
|
||||
let battery_slot = this._settings.get_int('battery-slot');
|
||||
|
||||
// addresses issue #161
|
||||
let battery_key = 'BAT'; // BAT0, BAT1 and BAT2
|
||||
if (battery_slot == 3) {
|
||||
battery_key = 'CMB'; // CMB0
|
||||
battery_slot = 0;
|
||||
} else if (battery_slot == 4) {
|
||||
battery_key = 'macsmc-battery'; // supports Asahi linux
|
||||
battery_slot = '';
|
||||
}
|
||||
|
||||
// uevent has all necessary fields, no need to read individual files
|
||||
let battery_path = '/sys/class/power_supply/' + battery_key + battery_slot + '/uevent';
|
||||
new FileModule.File(battery_path).read("\n").then(lines => {
|
||||
let output = {};
|
||||
for (let line of lines) {
|
||||
let split = line.split('=');
|
||||
output[split[0].replace('POWER_SUPPLY_', '')] = split[1];
|
||||
}
|
||||
|
||||
if ('STATUS' in output) {
|
||||
this._returnValue(callback, 'State', output['STATUS'], 'battery', '');
|
||||
}
|
||||
|
||||
if ('CYCLE_COUNT' in output) {
|
||||
this._returnValue(callback, 'Cycles', output['CYCLE_COUNT'], 'battery', '');
|
||||
}
|
||||
|
||||
if ('VOLTAGE_NOW' in output) {
|
||||
this._returnValue(callback, 'Voltage', output['VOLTAGE_NOW'] / 1000, 'battery', 'in');
|
||||
}
|
||||
|
||||
if ('CAPACITY_LEVEL' in output) {
|
||||
this._returnValue(callback, 'Level', output['CAPACITY_LEVEL'], 'battery', '');
|
||||
}
|
||||
|
||||
if ('CAPACITY' in output) {
|
||||
this._returnValue(callback, 'Percentage', output['CAPACITY'] / 100, 'battery', 'percent');
|
||||
}
|
||||
|
||||
if ('VOLTAGE_NOW' in output && 'CURRENT_NOW' in output && (!('POWER_NOW' in output))) {
|
||||
output['POWER_NOW'] = (output['VOLTAGE_NOW'] * output['CURRENT_NOW']) / 1000000;
|
||||
}
|
||||
|
||||
if ('POWER_NOW' in output) {
|
||||
this._returnValue(callback, 'Rate', output['POWER_NOW'], 'battery', 'watt');
|
||||
this._returnValue(callback, 'battery', output['POWER_NOW'], 'battery-group', 'watt');
|
||||
}
|
||||
|
||||
if ('CHARGE_FULL' in output && 'VOLTAGE_MIN_DESIGN' in output && (!('ENERGY_FULL' in output))) {
|
||||
output['ENERGY_FULL'] = (output['CHARGE_FULL'] * output['VOLTAGE_MIN_DESIGN']) / 1000000;
|
||||
}
|
||||
|
||||
if ('ENERGY_FULL' in output) {
|
||||
this._returnValue(callback, 'Energy (full)', output['ENERGY_FULL'], 'battery', 'watt-hour');
|
||||
}
|
||||
|
||||
if ('CHARGE_FULL_DESIGN' in output && 'VOLTAGE_MIN_DESIGN' in output && (!('ENERGY_FULL_DESIGN' in output))) {
|
||||
output['ENERGY_FULL_DESIGN'] = (output['CHARGE_FULL_DESIGN'] * output['VOLTAGE_MIN_DESIGN']) / 1000000;
|
||||
}
|
||||
|
||||
if ('ENERGY_FULL_DESIGN' in output) {
|
||||
this._returnValue(callback, 'Energy (design)', output['ENERGY_FULL_DESIGN'], 'battery', 'watt-hour');
|
||||
|
||||
if ('ENERGY_FULL' in output) {
|
||||
this._returnValue(callback, 'Capacity', (output['ENERGY_FULL'] / output['ENERGY_FULL_DESIGN']), 'battery', 'percent');
|
||||
}
|
||||
}
|
||||
|
||||
if ('VOLTAGE_MIN_DESIGN' in output && 'CHARGE_NOW' in output && (!('ENERGY_NOW' in output))) {
|
||||
output['ENERGY_NOW'] = (output['VOLTAGE_MIN_DESIGN'] * output['CHARGE_NOW']) / 1000000;
|
||||
}
|
||||
|
||||
if ('ENERGY_NOW' in output) {
|
||||
this._returnValue(callback, 'Energy (now)', output['ENERGY_NOW'], 'battery', 'watt-hour');
|
||||
}
|
||||
|
||||
if ('ENERGY_FULL' in output && 'ENERGY_NOW' in output && 'POWER_NOW' in output && output['POWER_NOW'] > 0 && 'STATUS' in output && (output['STATUS'] == 'Charging' || output['STATUS'] == 'Discharging')) {
|
||||
|
||||
let timeLeft = 0;
|
||||
|
||||
// two different formulas depending on if we are charging or discharging
|
||||
if (output['STATUS'] == 'Charging') {
|
||||
timeLeft = ((output['ENERGY_FULL'] - output['ENERGY_NOW']) / output['POWER_NOW']);
|
||||
} else {
|
||||
timeLeft = (output['ENERGY_NOW'] / output['POWER_NOW']);
|
||||
}
|
||||
|
||||
// don't process Infinity values
|
||||
if (timeLeft !== Infinity) {
|
||||
if (this._battery_charge_status != output['STATUS']) {
|
||||
// clears history due to state change
|
||||
this._battery_time_left_history = [];
|
||||
|
||||
// clear time left history when laptop goes in and out of charging
|
||||
this._battery_charge_status = output['STATUS'];
|
||||
}
|
||||
|
||||
// add latest time left estimate to our history
|
||||
this._battery_time_left_history.push(parseInt(timeLeft * 3600));
|
||||
|
||||
// keep track of last 15 time left estimates by erasing the first
|
||||
if (this._battery_time_left_history.length > 10)
|
||||
this._battery_time_left_history.shift();
|
||||
|
||||
// sum up and create average of our time left history
|
||||
let sum = this._battery_time_left_history.reduce((a, b) => a + b);
|
||||
let avg = sum / this._battery_time_left_history.length;
|
||||
|
||||
// use time left history to update screen
|
||||
this._returnValue(callback, 'Time left', parseInt(avg), 'battery', 'runtime');
|
||||
}
|
||||
} else {
|
||||
this._returnValue(callback, 'Time left', output['STATUS'], 'battery', '');
|
||||
}
|
||||
}).catch(err => { });
|
||||
}
|
||||
|
||||
_returnValue(callback, label, value, type, format) {
|
||||
// don't return if value is not a number - will revisit later
|
||||
//if (isNaN(value)) return;
|
||||
callback(label, value, type, format);
|
||||
}
|
||||
|
||||
_discoverHardwareMonitors(callback) {
|
||||
this._tempVoltFanSensors = { 'temperature': {}, 'voltage': {}, 'fan': {} };
|
||||
|
||||
let hwbase = '/sys/class/hwmon/';
|
||||
|
||||
// process sensor_types now so it is not called multiple times below
|
||||
let sensor_types = {};
|
||||
|
||||
if (this._settings.get_boolean('show-temperature'))
|
||||
sensor_types['temp'] = 'temperature';
|
||||
|
||||
if (this._settings.get_boolean('show-voltage'))
|
||||
sensor_types['in'] = 'voltage';
|
||||
|
||||
if (this._settings.get_boolean('show-fan'))
|
||||
sensor_types['fan'] = 'fan';
|
||||
|
||||
// a little informal, but this code has zero I/O block
|
||||
new FileModule.File(hwbase).list().then(files => {
|
||||
for (let file of files) {
|
||||
// grab name of sensor
|
||||
new FileModule.File(hwbase + file + '/name').read().then(name => {
|
||||
// are we dealing with a CPU?
|
||||
if (name == 'coretemp') {
|
||||
// determine which processor (socket) we are dealing with
|
||||
new FileModule.File(hwbase + file + '/temp1_label').read().then(prefix => {
|
||||
this._processTempVoltFan(callback, sensor_types, prefix, hwbase + file, file);
|
||||
}).catch(err => {
|
||||
// this shouldn't be necessary, but just in case temp1_label doesn't exist
|
||||
// attempt to fix #266
|
||||
this._processTempVoltFan(callback, sensor_types, name, hwbase + file, file);
|
||||
});
|
||||
} else {
|
||||
// not a CPU, process all other sensors
|
||||
this._processTempVoltFan(callback, sensor_types, name, hwbase + file, file);
|
||||
}
|
||||
}).catch(err => {
|
||||
new FileModule.File(hwbase + file + '/device/name').read().then(name => {
|
||||
this._processTempVoltFan(callback, sensor_types, name, hwbase + file + '/device', file);
|
||||
}).catch(err => { });
|
||||
});
|
||||
}
|
||||
}).catch(err => { });
|
||||
|
||||
// does this system support cpu scaling? if so we will use it to grab Frequency and Boost below
|
||||
new FileModule.File('/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq').read().then(value => {
|
||||
this._processor_uses_cpu_info = false;
|
||||
}).catch(err => { });
|
||||
|
||||
// is static CPU information enabled?
|
||||
if (this._settings.get_boolean('include-static-info')) {
|
||||
// grab static CPU information
|
||||
new FileModule.File('/proc/cpuinfo').read("\n").then(lines => {
|
||||
let vendor_id = '';
|
||||
let bogomips = '';
|
||||
let sockets = {};
|
||||
let cache = '';
|
||||
|
||||
for (let line of lines) {
|
||||
let value = '';
|
||||
|
||||
// grab cpu vendor
|
||||
if (value = line.match(/^vendor_id(\s+): (\w+.*)/)) vendor_id = value[2];
|
||||
|
||||
// grab bogomips
|
||||
if (value = line.match(/^bogomips(\s+): (\d*\.?\d*)$/)) bogomips = value[2];
|
||||
|
||||
// grab processor count
|
||||
if (value = line.match(/^physical id(\s+): (\d+)$/)) sockets[value[2]] = 1;
|
||||
|
||||
// grab cache
|
||||
if (value = line.match(/^cache size(\s+): (\d+) KB$/)) cache = value[2];
|
||||
}
|
||||
|
||||
this._returnValue(callback, 'Vendor', vendor_id, 'processor', 'string');
|
||||
this._returnValue(callback, 'Bogomips', bogomips, 'processor', 'string');
|
||||
this._returnValue(callback, 'Sockets', Object.keys(sockets).length, 'processor', 'string');
|
||||
this._returnValue(callback, 'Cache', cache, 'processor', 'memory');
|
||||
}).catch(err => { });
|
||||
|
||||
// grab static CPU information
|
||||
new FileModule.File('/proc/version').read(' ').then(kernelArray => {
|
||||
this._returnValue(callback, 'Kernel', kernelArray[2], 'system', 'string');
|
||||
}).catch(err => { });
|
||||
}
|
||||
}
|
||||
|
||||
_processTempVoltFan(callback, sensor_types, name, path, file) {
|
||||
let sensor_files = [ 'input', 'label' ];
|
||||
|
||||
// grab files from directory
|
||||
new FileModule.File(path).list().then(files2 => {
|
||||
let trisensors = {};
|
||||
|
||||
// loop over files from directory
|
||||
for (let file2 of Object.values(files2)) {
|
||||
// simple way of processing input and label (from above)
|
||||
for (let key of Object.values(sensor_files)) {
|
||||
// process toggled on sensors from extension preferences
|
||||
for (let sensor_type in sensor_types) {
|
||||
if (file2.substr(0, sensor_type.length) == sensor_type && file2.substr(-(key.length+1)) == '_' + key) {
|
||||
let key2 = file + file2.substr(0, file2.indexOf('_'));
|
||||
|
||||
if (!(key2 in trisensors)) {
|
||||
trisensors[key2] = {
|
||||
'type': sensor_types[sensor_type],
|
||||
'format': sensor_type,
|
||||
'label': path + '/name'
|
||||
};
|
||||
}
|
||||
|
||||
trisensors[key2][key] = path + '/' + file2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let obj of Object.values(trisensors)) {
|
||||
if (!('input' in obj))
|
||||
continue;
|
||||
|
||||
new FileModule.File(obj['input']).read().then(value => {
|
||||
let extra = (obj['label'].indexOf('_label')==-1) ? ' ' + obj['input'].substr(obj['input'].lastIndexOf('/')+1).split('_')[0] : '';
|
||||
|
||||
if (value > 0 || !this._settings.get_boolean('hide-zeros') || obj['type'] == 'fan') {
|
||||
new FileModule.File(obj['label']).read().then(label => {
|
||||
this._addTempVoltFan(callback, obj, name, label, extra, value);
|
||||
}).catch(err => {
|
||||
let tmpFile = obj['label'].substr(0, obj['label'].lastIndexOf('/')) + '/name';
|
||||
new FileModule.File(tmpFile).read().then(label => {
|
||||
this._addTempVoltFan(callback, obj, name, label, extra, value);
|
||||
}).catch(err => { });
|
||||
});
|
||||
}
|
||||
}).catch(err => { });
|
||||
}
|
||||
}).catch(err => { });
|
||||
}
|
||||
|
||||
_addTempVoltFan(callback, obj, name, label, extra, value) {
|
||||
// prepend module that provided sensor data
|
||||
if (name != label) label = name + ' ' + label;
|
||||
|
||||
//if (label == 'nvme Composite') label = 'NVMe';
|
||||
//if (label == 'nouveau') label = 'Nvidia';
|
||||
|
||||
label = label + extra;
|
||||
|
||||
// in the future we will read /etc/sensors3.conf
|
||||
if (label == 'acpitz temp1') label = 'ACPI Thermal Zone';
|
||||
if (label == 'pch_cannonlake temp1') label = 'Platform Controller Hub';
|
||||
if (label == 'iwlwifi_1 temp1') label = 'Wireless Adapter';
|
||||
if (label == 'Package id 0') label = 'Processor 0';
|
||||
if (label == 'Package id 1') label = 'Processor 1';
|
||||
label = label.replace('Package id', 'CPU');
|
||||
|
||||
let types = [ 'temperature', 'voltage', 'fan' ];
|
||||
for (let type of types) {
|
||||
// check if this label already exists
|
||||
if (label in this._tempVoltFanSensors[type]) {
|
||||
for (let i = 2; i <= 9; i++) {
|
||||
// append an incremented number to end
|
||||
let new_label = label + ' ' + i;
|
||||
|
||||
// if new label is available, use it
|
||||
if (!(new_label in this._tempVoltFanSensors[type])) {
|
||||
label = new_label;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update screen on initial build to prevent delay on update
|
||||
this._returnValue(callback, label, value, obj['type'], obj['format']);
|
||||
|
||||
this._tempVoltFanSensors[obj['type']][label] = {
|
||||
'format': obj['format'],
|
||||
'path': obj['input']
|
||||
};
|
||||
}
|
||||
|
||||
resetHistory() {
|
||||
this._next_public_ip_check = 0;
|
||||
this._hardware_detected = false;
|
||||
this._processor_uses_cpu_info = true;
|
||||
this._battery_time_left_history = [];
|
||||
this._battery_charge_status = '';
|
||||
}
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
.vitals-icon { icon-size: 16px; }
|
||||
.vitals-menu-button-container {}
|
||||
.vitals-panel-icon-temperature { margin: 0 1px 0 8px; padding: 0; }
|
||||
.vitals-panel-icon-voltage { margin: 0 0 0 8px; padding: 0; }
|
||||
.vitals-panel-icon-fan { margin: 0 4px 0 8px; padding: 0; }
|
||||
.vitals-panel-icon-memory { margin: 0 2px 0 8px; padding: 0; }
|
||||
.vitals-panel-icon-processor { margin: 0 3px 0 8px; padding: 0; }
|
||||
.vitals-panel-icon-system { margin: 0 3px 0 8px; padding: 0; }
|
||||
.vitals-panel-icon-network { margin: 0 3px 0 8px; padding: 0; }
|
||||
.vitals-panel-icon-storage { margin: 0 2px 0 8px; padding: 0; }
|
||||
.vitals-panel-icon-battery { margin: 0 4px 0 8px; padding: 0; }
|
||||
.vitals-panel-label { margin: 0 3px 0 0; padding: 0; }
|
||||
.vitals-button-action { -st-icon-style: symbolic; border-radius: 32px; margin: 0px; min-height: 22px; min-width: 22px; padding: 10px; font-size: 100%; border: 1px solid transparent; }
|
||||
.vitals-button-action:hover, .vitals-button-action:focus { border-color: #777; }
|
||||
.vitals-button-action > StIcon { icon-size: 16px; }
|
||||
.vitals-button-box { padding: 0px; spacing: 22px; }
|
|
@ -0,0 +1,337 @@
|
|||
/*
|
||||
Copyright (c) 2018, Chris Monahan <chris@corecoding.com>
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the GNOME nor the names of its contributors may be
|
||||
used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
import GObject from 'gi://GObject';
|
||||
|
||||
const cbFun = (d, c) => {
|
||||
let bb = d[1] % c[0],
|
||||
aa = (d[1] - bb) / c[0];
|
||||
aa = aa > 0 ? aa + c[1] : '';
|
||||
|
||||
return [d[0] + aa, bb];
|
||||
};
|
||||
|
||||
export const Values = GObject.registerClass({
|
||||
GTypeName: 'Values',
|
||||
}, class Values extends GObject.Object {
|
||||
|
||||
_init(settings, sensorIcons) {
|
||||
this._settings = settings;
|
||||
this._sensorIcons = sensorIcons;
|
||||
|
||||
this._networkSpeedOffset = {};
|
||||
this._networkSpeeds = {};
|
||||
|
||||
this._history = {};
|
||||
//this._history2 = {};
|
||||
this.resetHistory();
|
||||
}
|
||||
|
||||
_legible(value, sensorClass) {
|
||||
let unit = 1000;
|
||||
if (value === null) return 'N/A';
|
||||
let use_higher_precision = this._settings.get_boolean('use-higher-precision');
|
||||
let memory_measurement = this._settings.get_int('memory-measurement')
|
||||
let storage_measurement = this._settings.get_int('storage-measurement')
|
||||
let use_bps = (this._settings.get_int('network-speed-format') == 1);
|
||||
|
||||
let format = '';
|
||||
let ending = '';
|
||||
let exp = 0;
|
||||
|
||||
var decimal = [ 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ];
|
||||
var binary = [ 'B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB' ];
|
||||
var hertz = [ 'Hz', 'KHz', 'MHz', 'GHz', 'THz', 'PHz', 'EHz', 'ZHz' ];
|
||||
|
||||
switch (sensorClass) {
|
||||
case 'percent':
|
||||
format = (use_higher_precision)?'%.1f%s':'%d%s';
|
||||
value = value * 100;
|
||||
if (value > 100) value = 100;
|
||||
ending = '%';
|
||||
break;
|
||||
case 'temp':
|
||||
value = value / 1000;
|
||||
ending = '°C';
|
||||
|
||||
// are we converting to fahrenheit?
|
||||
if (this._settings.get_int('unit') == 1) {
|
||||
value = ((9 / 5) * value + 32);
|
||||
ending = '°F';
|
||||
}
|
||||
|
||||
format = (use_higher_precision)?'%.1f%s':'%d%s';
|
||||
break;
|
||||
case 'fan':
|
||||
format = '%d %s';
|
||||
ending = 'RPM';
|
||||
break;
|
||||
case 'in': // voltage
|
||||
value = value / 1000;
|
||||
format = ((value >= 0) ? '+' : '-') + ((use_higher_precision)?'%.2f %s':'%.1f %s');
|
||||
ending = 'V';
|
||||
break;
|
||||
case 'hertz':
|
||||
if (value > 0) {
|
||||
exp = Math.floor(Math.log(value) / Math.log(unit));
|
||||
if (value >= Math.pow(unit, exp) * (unit - 0.05)) exp++;
|
||||
value = parseFloat((value / Math.pow(unit, exp)));
|
||||
}
|
||||
|
||||
format = (use_higher_precision)?'%.2f %s':'%.1f %s';
|
||||
ending = hertz[exp];
|
||||
break;
|
||||
case 'memory':
|
||||
unit = (memory_measurement)?1000:1024;
|
||||
|
||||
if (value > 0) {
|
||||
value *= unit;
|
||||
exp = Math.floor(Math.log(value) / Math.log(unit));
|
||||
if (value >= Math.pow(unit, exp) * (unit - 0.05)) exp++;
|
||||
value = parseFloat((value / Math.pow(unit, exp)));
|
||||
}
|
||||
|
||||
format = (use_higher_precision)?'%.2f %s':'%.1f %s';
|
||||
|
||||
if (memory_measurement)
|
||||
ending = decimal[exp];
|
||||
else
|
||||
ending = binary[exp];
|
||||
|
||||
break;
|
||||
case 'storage':
|
||||
unit = (storage_measurement)?1000:1024;
|
||||
|
||||
if (value > 0) {
|
||||
exp = Math.floor(Math.log(value) / Math.log(unit));
|
||||
if (value >= Math.pow(unit, exp) * (unit - 0.05)) exp++;
|
||||
value = parseFloat((value / Math.pow(unit, exp)));
|
||||
}
|
||||
|
||||
format = (use_higher_precision)?'%.2f %s':'%.1f %s';
|
||||
|
||||
if (storage_measurement)
|
||||
ending = decimal[exp];
|
||||
else
|
||||
ending = binary[exp];
|
||||
|
||||
break;
|
||||
case 'speed':
|
||||
if (value > 0) {
|
||||
if (use_bps) value *= 8;
|
||||
exp = Math.floor(Math.log(value) / Math.log(unit));
|
||||
if (value >= Math.pow(unit, exp) * (unit - 0.05)) exp++;
|
||||
value = parseFloat((value / Math.pow(unit, exp)));
|
||||
}
|
||||
|
||||
format = (use_higher_precision)?'%.1f %s':'%.0f %s';
|
||||
|
||||
if (use_bps) {
|
||||
ending = decimal[exp].replace('B', 'bps');
|
||||
} else {
|
||||
ending = decimal[exp] + '/s';
|
||||
}
|
||||
|
||||
break;
|
||||
case 'runtime':
|
||||
case 'uptime':
|
||||
let scale = [24, 60, 60];
|
||||
let units = ['d ', 'h ', 'm '];
|
||||
|
||||
// show seconds on higher precision or if value under a minute
|
||||
if (sensorClass != 'runtime' && (use_higher_precision || value < 60)) {
|
||||
scale.push(1);
|
||||
units.push('s ');
|
||||
}
|
||||
|
||||
let rslt = scale.map((d, i, a) => a.slice(i).reduce((d, c) => d * c))
|
||||
.map((d, i) => ([d, units[i]]))
|
||||
.reduce(cbFun, ['', value]);
|
||||
|
||||
value = rslt[0].trim();
|
||||
|
||||
format = '%s';
|
||||
break;
|
||||
case 'milliamp':
|
||||
format = (use_higher_precision)?'%.1f %s':'%d %s';
|
||||
value = value / 1000;
|
||||
ending = 'mA';
|
||||
break;
|
||||
case 'milliamp-hour':
|
||||
format = (use_higher_precision)?'%.1f %s':'%d %s';
|
||||
value = value / 1000;
|
||||
ending = 'mAh';
|
||||
break;
|
||||
case 'watt':
|
||||
format = (use_higher_precision)?'%.2f %s':'%.1f %s';
|
||||
value = value / 1000000;
|
||||
ending = 'W';
|
||||
break;
|
||||
case 'watt-hour':
|
||||
format = (use_higher_precision)?'%.2f %s':'%.1f %s';
|
||||
value = value / 1000000;
|
||||
ending = 'Wh';
|
||||
break;
|
||||
case 'load':
|
||||
format = (use_higher_precision)?'%.2f %s':'%.1f %s';
|
||||
break;
|
||||
default:
|
||||
format = '%s';
|
||||
break;
|
||||
}
|
||||
|
||||
return format.format(value, ending);
|
||||
}
|
||||
|
||||
returnIfDifferent(dwell, label, value, type, format, key) {
|
||||
let output = [];
|
||||
|
||||
// make sure the keys exist
|
||||
if (!(type in this._history)) this._history[type] = {};
|
||||
|
||||
// no sense in continuing when the raw value has not changed
|
||||
if (type != 'network-rx' && type != 'network-tx' &&
|
||||
key in this._history[type] && this._history[type][key][1] == value)
|
||||
return output;
|
||||
|
||||
// is the value different from last time?
|
||||
let legible = this._legible(value, format);
|
||||
|
||||
// don't return early when dealing with network traffic
|
||||
if (type != 'network-rx' && type != 'network-tx') {
|
||||
// only update when we are coming through for the first time, or if a value has changed
|
||||
if (key in this._history[type] && this._history[type][key][0] == legible)
|
||||
return output;
|
||||
|
||||
// add label as it was sent from sensors class
|
||||
output.push([label, legible, type, key]);
|
||||
}
|
||||
|
||||
// save previous values to update screen on changes only
|
||||
let previousValue = this._history[type][key];
|
||||
this._history[type][key] = [legible, value];
|
||||
|
||||
// process average, min and max values
|
||||
if (type == 'temperature' || type == 'voltage' || type == 'fan') {
|
||||
let vals = Object.values(this._history[type]).map(x => parseFloat(x[1]));
|
||||
|
||||
// show value in group even if there is one value present
|
||||
let sum = vals.reduce((a, b) => a + b);
|
||||
let avg = this._legible(sum / vals.length, format);
|
||||
output.push([type, avg, type + '-group', '']);
|
||||
|
||||
// If only one value is present, don't display avg, min and max
|
||||
if (vals.length > 1) {
|
||||
output.push(['Average', avg, type, '__' + type + '_avg__']);
|
||||
|
||||
// calculate Minimum value
|
||||
let min = Math.min(...vals);
|
||||
min = this._legible(min, format);
|
||||
output.push(['Minimum', min, type, '__' + type + '_min__']);
|
||||
|
||||
// calculate Maximum value
|
||||
let max = Math.max(...vals);
|
||||
max = this._legible(max, format);
|
||||
output.push(['Maximum', max, type, '__' + type + '_max__']);
|
||||
}
|
||||
} else if (type == 'network-rx' || type == 'network-tx') {
|
||||
let direction = type.split('-')[1];
|
||||
|
||||
// appends total upload and download for all interfaces for #216
|
||||
let vals = Object.values(this._history[type]).map(x => parseFloat(x[1]));
|
||||
let sum = vals.reduce((partialSum, a) => partialSum + a, 0);
|
||||
output.push(['Boot ' + direction, this._legible(sum, format), type, '__' + type + '_boot__']);
|
||||
|
||||
// keeps track of session start point
|
||||
if (!(key in this._networkSpeedOffset) || this._networkSpeedOffset[key] <= 0)
|
||||
this._networkSpeedOffset[key] = sum;
|
||||
|
||||
// outputs session upload and download for all interfaces for #234
|
||||
output.push(['Session ' + direction, this._legible(sum - this._networkSpeedOffset[key], format), type, '__' + type + '_ses__']);
|
||||
|
||||
// calculate speed for this interface
|
||||
let speed = (value - previousValue[1]) / dwell;
|
||||
output.push([label, this._legible(speed, 'speed'), type, key]);
|
||||
|
||||
// store speed for Device report
|
||||
if (!(direction in this._networkSpeeds)) this._networkSpeeds[direction] = {};
|
||||
if (!(label in this._networkSpeeds[direction])) this._networkSpeeds[direction][label] = 0;
|
||||
|
||||
// store value for next go around
|
||||
if (value > 0 || (value == 0 && !this._settings.get_boolean('hide-zeros')))
|
||||
this._networkSpeeds[direction][label] = speed;
|
||||
|
||||
// calculate total upload and download device speed
|
||||
for (let direction in this._networkSpeeds) {
|
||||
let sum = 0;
|
||||
for (let iface in this._networkSpeeds[direction])
|
||||
sum += parseFloat(this._networkSpeeds[direction][iface]);
|
||||
|
||||
sum = this._legible(sum, 'speed');
|
||||
output.push(['Device ' + direction, sum, 'network-' + direction, '__network-' + direction + '_max__']);
|
||||
// append download speed to group itself
|
||||
if (direction == 'rx') output.push([type, sum, type + '-group', '']);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
global.log('before', JSON.stringify(output));
|
||||
for (let i = output.length - 1; i >= 0; i--) {
|
||||
let sensor = output[i];
|
||||
// sensor[0]=label, sensor[1]=value, sensor[2]=type, sensor[3]=key)
|
||||
|
||||
//["CPU Core 5","46°C","temperature","_temperature_hwmon8temp7_"]
|
||||
|
||||
// make sure the keys exist
|
||||
if (!(sensor[2] in this._history2)) this._history2[sensor[2]] = {};
|
||||
|
||||
if (sensor[3] in this._history2[sensor[2]]) {
|
||||
if (this._history2[sensor[2]][sensor[3]] == sensor[1]) {
|
||||
output.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
this._history2[sensor[2]][sensor[3]] = sensor[1];
|
||||
}
|
||||
|
||||
global.log(' after', JSON.stringify(output));
|
||||
global.log('***************************');
|
||||
*/
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
resetHistory() {
|
||||
// don't call this._history = {}, as we want to keep network-rx and network-tx
|
||||
// otherwise network history statistics will start over
|
||||
for (let sensor in this._sensorIcons) {
|
||||
this._history[sensor] = {};
|
||||
this._history[sensor + '-group'] = {};
|
||||
//this._history2[sensor] = {};
|
||||
//this._history2[sensor + '-group'] = {};
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,950 @@
|
|||
// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import Clutter from 'gi://Clutter';
|
||||
import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
import GdkPixbuf from 'gi://GdkPixbuf';
|
||||
import Gio from 'gi://Gio';
|
||||
import St from 'gi://St';
|
||||
|
||||
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
|
||||
import * as Signals from 'resource:///org/gnome/shell/misc/signals.js';
|
||||
|
||||
import * as DBusInterfaces from './interfaces.js';
|
||||
import * as PromiseUtils from './promiseUtils.js';
|
||||
import * as Util from './util.js';
|
||||
import {DBusProxy} from './dbusProxy.js';
|
||||
|
||||
Gio._promisify(GdkPixbuf.Pixbuf, 'new_from_stream_async');
|
||||
|
||||
// ////////////////////////////////////////////////////////////////////////
|
||||
// PART ONE: "ViewModel" backend implementation.
|
||||
// Both code and design are inspired by libdbusmenu
|
||||
// ////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Saves menu property values and handles type checking and defaults
|
||||
*/
|
||||
export class PropertyStore {
|
||||
constructor(initialProperties) {
|
||||
this._props = new Map();
|
||||
|
||||
if (initialProperties) {
|
||||
for (const [prop, value] of Object.entries(initialProperties))
|
||||
this.set(prop, value);
|
||||
}
|
||||
}
|
||||
|
||||
set(name, value) {
|
||||
if (name in PropertyStore.MandatedTypes && value &&
|
||||
!value.is_of_type(PropertyStore.MandatedTypes[name]))
|
||||
Util.Logger.warn(`Cannot set property ${name}: type mismatch!`);
|
||||
else if (value)
|
||||
this._props.set(name, value);
|
||||
else
|
||||
this._props.delete(name);
|
||||
}
|
||||
|
||||
get(name) {
|
||||
const prop = this._props.get(name);
|
||||
if (prop)
|
||||
return prop;
|
||||
else if (name in PropertyStore.DefaultValues)
|
||||
return PropertyStore.DefaultValues[name];
|
||||
else
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// we list all the properties we know and use here, so we won' have to deal with unexpected type mismatches
|
||||
PropertyStore.MandatedTypes = {
|
||||
'visible': GLib.VariantType.new('b'),
|
||||
'enabled': GLib.VariantType.new('b'),
|
||||
'label': GLib.VariantType.new('s'),
|
||||
'type': GLib.VariantType.new('s'),
|
||||
'children-display': GLib.VariantType.new('s'),
|
||||
'icon-name': GLib.VariantType.new('s'),
|
||||
'icon-data': GLib.VariantType.new('ay'),
|
||||
'toggle-type': GLib.VariantType.new('s'),
|
||||
'toggle-state': GLib.VariantType.new('i'),
|
||||
};
|
||||
|
||||
PropertyStore.DefaultValues = {
|
||||
'visible': GLib.Variant.new_boolean(true),
|
||||
'enabled': GLib.Variant.new_boolean(true),
|
||||
'label': GLib.Variant.new_string(''),
|
||||
'type': GLib.Variant.new_string('standard'),
|
||||
// elements not in here must return null
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a single menu item
|
||||
*/
|
||||
export class DbusMenuItem extends Signals.EventEmitter {
|
||||
// will steal the properties object
|
||||
constructor(client, id, properties, childrenIds) {
|
||||
super();
|
||||
|
||||
this._client = client;
|
||||
this._id = id;
|
||||
this._propStore = new PropertyStore(properties);
|
||||
this._children_ids = childrenIds;
|
||||
}
|
||||
|
||||
propertyGet(propName) {
|
||||
const prop = this.propertyGetVariant(propName);
|
||||
return prop ? prop.get_string()[0] : null;
|
||||
}
|
||||
|
||||
propertyGetVariant(propName) {
|
||||
return this._propStore.get(propName);
|
||||
}
|
||||
|
||||
propertyGetBool(propName) {
|
||||
const prop = this.propertyGetVariant(propName);
|
||||
return prop ? prop.get_boolean() : false;
|
||||
}
|
||||
|
||||
propertyGetInt(propName) {
|
||||
const prop = this.propertyGetVariant(propName);
|
||||
return prop ? prop.get_int32() : 0;
|
||||
}
|
||||
|
||||
propertySet(prop, value) {
|
||||
this._propStore.set(prop, value);
|
||||
|
||||
this.emit('property-changed', prop, this.propertyGetVariant(prop));
|
||||
}
|
||||
|
||||
getChildrenIds() {
|
||||
return this._children_ids.concat(); // clone it!
|
||||
}
|
||||
|
||||
addChild(pos, childId) {
|
||||
this._children_ids.splice(pos, 0, childId);
|
||||
this.emit('child-added', this._client.getItem(childId), pos);
|
||||
}
|
||||
|
||||
removeChild(childId) {
|
||||
// find it
|
||||
let pos = -1;
|
||||
for (let i = 0; i < this._children_ids.length; ++i) {
|
||||
if (this._children_ids[i] === childId) {
|
||||
pos = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (pos < 0) {
|
||||
Util.Logger.critical("Trying to remove child which doesn't exist");
|
||||
} else {
|
||||
this._children_ids.splice(pos, 1);
|
||||
this.emit('child-removed', this._client.getItem(childId));
|
||||
}
|
||||
}
|
||||
|
||||
moveChild(childId, newPos) {
|
||||
// find the old position
|
||||
let oldPos = -1;
|
||||
for (let i = 0; i < this._children_ids.length; ++i) {
|
||||
if (this._children_ids[i] === childId) {
|
||||
oldPos = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldPos < 0) {
|
||||
Util.Logger.critical("tried to move child which wasn't in the list");
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldPos !== newPos) {
|
||||
this._children_ids.splice(oldPos, 1);
|
||||
this._children_ids.splice(newPos, 0, childId);
|
||||
this.emit('child-moved', oldPos, newPos, this._client.getItem(childId));
|
||||
}
|
||||
}
|
||||
|
||||
getChildren() {
|
||||
return this._children_ids.map(el => this._client.getItem(el));
|
||||
}
|
||||
|
||||
handleEvent(event, data, timestamp) {
|
||||
if (!data)
|
||||
data = GLib.Variant.new_int32(0);
|
||||
|
||||
this._client.sendEvent(this._id, event, data, timestamp);
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
sendAboutToShow() {
|
||||
this._client.sendAboutToShow(this._id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The client does the heavy lifting of actually reading layouts and distributing events
|
||||
*/
|
||||
|
||||
export const DBusClient = GObject.registerClass({
|
||||
Signals: {'ready-changed': {}},
|
||||
}, class AppIndicatorsDBusClient extends DBusProxy {
|
||||
static get interfaceInfo() {
|
||||
if (!this._interfaceInfo) {
|
||||
this._interfaceInfo = Gio.DBusInterfaceInfo.new_for_xml(
|
||||
DBusInterfaces.DBusMenu);
|
||||
}
|
||||
return this._interfaceInfo;
|
||||
}
|
||||
|
||||
static get baseItems() {
|
||||
if (!this._baseItems) {
|
||||
this._baseItems = {
|
||||
'children-display': GLib.Variant.new_string('submenu'),
|
||||
};
|
||||
}
|
||||
return this._baseItems;
|
||||
}
|
||||
|
||||
static destroy() {
|
||||
delete this._interfaceInfo;
|
||||
}
|
||||
|
||||
_init(busName, objectPath) {
|
||||
const {interfaceInfo} = AppIndicatorsDBusClient;
|
||||
|
||||
super._init(busName, objectPath, interfaceInfo,
|
||||
Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES);
|
||||
|
||||
this._items = new Map();
|
||||
this._items.set(0, new DbusMenuItem(this, 0, DBusClient.baseItems, []));
|
||||
this._flagItemsUpdateRequired = false;
|
||||
|
||||
// will be set to true if a layout update is needed once active
|
||||
this._flagLayoutUpdateRequired = false;
|
||||
|
||||
// property requests are queued
|
||||
this._propertiesRequestedFor = new Set(/* ids */);
|
||||
|
||||
this._layoutUpdated = false;
|
||||
this._active = false;
|
||||
}
|
||||
|
||||
async initAsync(cancellable) {
|
||||
await super.initAsync(cancellable);
|
||||
|
||||
this._requestLayoutUpdate();
|
||||
}
|
||||
|
||||
_onNameOwnerChanged() {
|
||||
if (this.isReady)
|
||||
this._requestLayoutUpdate();
|
||||
}
|
||||
|
||||
get isReady() {
|
||||
return this._layoutUpdated && !!this.gNameOwner;
|
||||
}
|
||||
|
||||
get cancellable() {
|
||||
return this._cancellable;
|
||||
}
|
||||
|
||||
getRoot() {
|
||||
return this._items.get(0);
|
||||
}
|
||||
|
||||
_requestLayoutUpdate() {
|
||||
const cancellable = new Util.CancellableChild(this._cancellable);
|
||||
this._beginLayoutUpdate(cancellable);
|
||||
}
|
||||
|
||||
async _requestProperties(propertyId, cancellable) {
|
||||
this._propertiesRequestedFor.add(propertyId);
|
||||
|
||||
if (this._propertiesRequest && this._propertiesRequest.pending())
|
||||
return;
|
||||
|
||||
// if we don't have any requests queued, we'll need to add one
|
||||
this._propertiesRequest = new PromiseUtils.IdlePromise(
|
||||
GLib.PRIORITY_DEFAULT_IDLE, cancellable);
|
||||
await this._propertiesRequest;
|
||||
|
||||
const requestedProperties = Array.from(this._propertiesRequestedFor);
|
||||
this._propertiesRequestedFor.clear();
|
||||
const [result] = await this.GetGroupPropertiesAsync(requestedProperties,
|
||||
[], cancellable);
|
||||
|
||||
result.forEach(([id, properties]) => {
|
||||
const item = this._items.get(id);
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
for (const [prop, value] of Object.entries(properties))
|
||||
item.propertySet(prop, value);
|
||||
});
|
||||
}
|
||||
|
||||
// Traverses the list of cached menu items and removes everyone that is not in the list
|
||||
// so we don't keep alive unused items
|
||||
_gcItems() {
|
||||
const tag = new Date().getTime();
|
||||
|
||||
const toTraverse = [0];
|
||||
while (toTraverse.length > 0) {
|
||||
const item = this.getItem(toTraverse.shift());
|
||||
item._dbusClientGcTag = tag;
|
||||
Array.prototype.push.apply(toTraverse, item.getChildrenIds());
|
||||
}
|
||||
|
||||
this._items.forEach((i, id) => {
|
||||
if (i._dbusClientGcTag !== tag)
|
||||
this._items.delete(id);
|
||||
});
|
||||
}
|
||||
|
||||
// the original implementation will only request partial layouts if somehow possible
|
||||
// we try to save us from multiple kinds of race conditions by always requesting a full layout
|
||||
_beginLayoutUpdate(cancellable) {
|
||||
this._layoutUpdateUpdateAsync(cancellable).catch(e => {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e);
|
||||
});
|
||||
}
|
||||
|
||||
// the original implementation will only request partial layouts if somehow possible
|
||||
// we try to save us from multiple kinds of race conditions by always requesting a full layout
|
||||
async _layoutUpdateUpdateAsync(cancellable) {
|
||||
// we only read the type property, because if the type changes after reading all properties,
|
||||
// the view would have to replace the item completely which we try to avoid
|
||||
if (this._layoutUpdateCancellable)
|
||||
this._layoutUpdateCancellable.cancel();
|
||||
|
||||
this._layoutUpdateCancellable = cancellable;
|
||||
|
||||
try {
|
||||
const [revision_, root] = await this.GetLayoutAsync(0, -1,
|
||||
['type', 'children-display'], cancellable);
|
||||
|
||||
this._updateLayoutState(true);
|
||||
this._doLayoutUpdate(root, cancellable);
|
||||
this._gcItems();
|
||||
this._flagLayoutUpdateRequired = false;
|
||||
this._flagItemsUpdateRequired = false;
|
||||
} catch (e) {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
this._updateLayoutState(false);
|
||||
throw e;
|
||||
} finally {
|
||||
if (this._layoutUpdateCancellable === cancellable)
|
||||
this._layoutUpdateCancellable = null;
|
||||
}
|
||||
}
|
||||
|
||||
_updateLayoutState(state) {
|
||||
const wasReady = this.isReady;
|
||||
this._layoutUpdated = state;
|
||||
if (this.isReady !== wasReady)
|
||||
this.emit('ready-changed');
|
||||
}
|
||||
|
||||
_doLayoutUpdate(item, cancellable) {
|
||||
const [id, properties, children] = item;
|
||||
|
||||
const childrenUnpacked = children.map(c => c.deep_unpack());
|
||||
const childrenIds = childrenUnpacked.map(([c]) => c);
|
||||
|
||||
// make sure all our children exist
|
||||
childrenUnpacked.forEach(c => this._doLayoutUpdate(c, cancellable));
|
||||
|
||||
// make sure we exist
|
||||
const menuItem = this._items.get(id);
|
||||
|
||||
if (menuItem) {
|
||||
// we do, update our properties if necessary
|
||||
for (const [prop, value] of Object.entries(properties))
|
||||
menuItem.propertySet(prop, value);
|
||||
|
||||
// make sure our children are all at the right place, and exist
|
||||
const oldChildrenIds = menuItem.getChildrenIds();
|
||||
for (let i = 0; i < childrenIds.length; ++i) {
|
||||
// try to recycle an old child
|
||||
let oldChild = -1;
|
||||
for (let j = 0; j < oldChildrenIds.length; ++j) {
|
||||
if (oldChildrenIds[j] === childrenIds[i]) {
|
||||
[oldChild] = oldChildrenIds.splice(j, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldChild < 0) {
|
||||
// no old child found, so create a new one!
|
||||
menuItem.addChild(i, childrenIds[i]);
|
||||
} else {
|
||||
// old child found, reuse it!
|
||||
menuItem.moveChild(childrenIds[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
// remove any old children that weren't reused
|
||||
oldChildrenIds.forEach(c => menuItem.removeChild(c));
|
||||
|
||||
if (!this._flagItemsUpdateRequired)
|
||||
return id;
|
||||
}
|
||||
|
||||
// we don't, so let's create us
|
||||
let newMenuItem = menuItem;
|
||||
|
||||
if (!newMenuItem) {
|
||||
newMenuItem = new DbusMenuItem(this, id, properties, childrenIds);
|
||||
this._items.set(id, newMenuItem);
|
||||
}
|
||||
|
||||
this._requestProperties(id, cancellable).catch(e => {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
Util.Logger.warn(`Could not get menu properties menu proxy: ${e}`);
|
||||
});
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
async _doPropertiesUpdateAsync(cancellable) {
|
||||
if (this._propertiesUpdateCancellable)
|
||||
this._propertiesUpdateCancellable.cancel();
|
||||
|
||||
this._propertiesUpdateCancellable = cancellable;
|
||||
|
||||
try {
|
||||
const requests = [];
|
||||
|
||||
this._items.forEach((_, id) =>
|
||||
requests.push(this._requestProperties(id, cancellable)));
|
||||
|
||||
await Promise.all(requests);
|
||||
} finally {
|
||||
if (this._propertiesUpdateCancellable === cancellable)
|
||||
this._propertiesUpdateCancellable = null;
|
||||
}
|
||||
}
|
||||
|
||||
_doPropertiesUpdate() {
|
||||
const cancellable = new Util.CancellableChild(this._cancellable);
|
||||
this._doPropertiesUpdateAsync(cancellable).catch(e => {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
Util.Logger.warn(`Could not get menu properties menu proxy: ${e}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
set active(active) {
|
||||
const wasActive = this._active;
|
||||
this._active = active;
|
||||
|
||||
if (active && wasActive !== active) {
|
||||
if (this._flagLayoutUpdateRequired) {
|
||||
this._requestLayoutUpdate();
|
||||
} else if (this._flagItemsUpdateRequired) {
|
||||
this._doPropertiesUpdate();
|
||||
this._flagItemsUpdateRequired = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onSignal(_sender, signal, params) {
|
||||
if (signal === 'LayoutUpdated') {
|
||||
if (!this._active) {
|
||||
this._flagLayoutUpdateRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this._requestLayoutUpdate();
|
||||
} else if (signal === 'ItemsPropertiesUpdated') {
|
||||
if (!this._active) {
|
||||
this._flagItemsUpdateRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this._onPropertiesUpdated(params.deep_unpack());
|
||||
}
|
||||
}
|
||||
|
||||
getItem(id) {
|
||||
const item = this._items.get(id);
|
||||
if (!item)
|
||||
Util.Logger.warn(`trying to retrieve item for non-existing id ${id} !?`);
|
||||
return item || null;
|
||||
}
|
||||
|
||||
// we don't need to cache and burst-send that since it will not happen that frequently
|
||||
async sendAboutToShow(id) {
|
||||
/* Some indicators (you, dropbox!) don't use the right signature
|
||||
* and don't return a boolean, so we need to support both cases */
|
||||
try {
|
||||
const ret = await this.gConnection.call(this.gName, this.gObjectPath,
|
||||
this.gInterfaceName, 'AboutToShow', new GLib.Variant('(i)', [id]),
|
||||
null, Gio.DBusCallFlags.NONE, -1, this._cancellable);
|
||||
|
||||
if ((ret.is_of_type(new GLib.VariantType('(b)')) &&
|
||||
ret.get_child_value(0).get_boolean()) ||
|
||||
ret.is_of_type(new GLib.VariantType('()')))
|
||||
this._requestLayoutUpdate();
|
||||
} catch (e) {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
sendEvent(id, event, params, timestamp) {
|
||||
if (!this.gNameOwner)
|
||||
return;
|
||||
|
||||
this.EventAsync(id, event, params, timestamp, this._cancellable).catch(e => {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e);
|
||||
});
|
||||
}
|
||||
|
||||
_onPropertiesUpdated([changed, removed]) {
|
||||
changed.forEach(([id, props]) => {
|
||||
const item = this._items.get(id);
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
for (const [prop, value] of Object.entries(props))
|
||||
item.propertySet(prop, value);
|
||||
});
|
||||
removed.forEach(([id, propNames]) => {
|
||||
const item = this._items.get(id);
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
propNames.forEach(propName => item.propertySet(propName, null));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ////////////////////////////////////////////////////////////////////////
|
||||
// PART TWO: "View" frontend implementation.
|
||||
// ////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// https://bugzilla.gnome.org/show_bug.cgi?id=731514
|
||||
// GNOME 3.10 and 3.12 can't open a nested submenu.
|
||||
// Patches have been written, but it's not clear when (if?) they will be applied.
|
||||
// We also don't know whether they will be backported to 3.10, so we will work around
|
||||
// it in the meantime. Offending versions can be clearly identified:
|
||||
const NEED_NESTED_SUBMENU_FIX = '_setOpenedSubMenu' in PopupMenu.PopupMenu.prototype;
|
||||
|
||||
/**
|
||||
* Creates new wrapper menu items and injects methods for managing them at runtime.
|
||||
*
|
||||
* Many functions in this object will be bound to the created item and executed as event
|
||||
* handlers, so any `this` will refer to a menu item create in createItem
|
||||
*/
|
||||
const MenuItemFactory = {
|
||||
createItem(client, dbusItem) {
|
||||
// first, decide whether it's a submenu or not
|
||||
let shellItem;
|
||||
if (dbusItem.propertyGet('children-display') === 'submenu')
|
||||
shellItem = new PopupMenu.PopupSubMenuMenuItem('FIXME');
|
||||
else if (dbusItem.propertyGet('type') === 'separator')
|
||||
shellItem = new PopupMenu.PopupSeparatorMenuItem('');
|
||||
else
|
||||
shellItem = new PopupMenu.PopupMenuItem('FIXME');
|
||||
|
||||
shellItem._dbusItem = dbusItem;
|
||||
shellItem._dbusClient = client;
|
||||
|
||||
if (shellItem instanceof PopupMenu.PopupMenuItem) {
|
||||
shellItem._icon = new St.Icon({
|
||||
style_class: 'popup-menu-icon',
|
||||
xAlign: Clutter.ActorAlign.END,
|
||||
});
|
||||
shellItem.add_child(shellItem._icon);
|
||||
shellItem.label.x_expand = true;
|
||||
}
|
||||
|
||||
// initialize our state
|
||||
MenuItemFactory._updateLabel.call(shellItem);
|
||||
MenuItemFactory._updateOrnament.call(shellItem);
|
||||
MenuItemFactory._updateImage.call(shellItem);
|
||||
MenuItemFactory._updateVisible.call(shellItem);
|
||||
MenuItemFactory._updateSensitive.call(shellItem);
|
||||
|
||||
// initially create children
|
||||
if (shellItem instanceof PopupMenu.PopupSubMenuMenuItem) {
|
||||
dbusItem.getChildren().forEach(c =>
|
||||
shellItem.menu.addMenuItem(MenuItemFactory.createItem(client, c)));
|
||||
}
|
||||
|
||||
// now, connect various events
|
||||
Util.connectSmart(dbusItem, 'property-changed',
|
||||
shellItem, MenuItemFactory._onPropertyChanged);
|
||||
Util.connectSmart(dbusItem, 'child-added',
|
||||
shellItem, MenuItemFactory._onChildAdded);
|
||||
Util.connectSmart(dbusItem, 'child-removed',
|
||||
shellItem, MenuItemFactory._onChildRemoved);
|
||||
Util.connectSmart(dbusItem, 'child-moved',
|
||||
shellItem, MenuItemFactory._onChildMoved);
|
||||
Util.connectSmart(shellItem, 'activate',
|
||||
shellItem, MenuItemFactory._onActivate);
|
||||
|
||||
shellItem.connect('destroy', () => {
|
||||
shellItem._dbusItem = null;
|
||||
shellItem._dbusClient = null;
|
||||
shellItem._icon = null;
|
||||
});
|
||||
|
||||
if (shellItem.menu) {
|
||||
Util.connectSmart(shellItem.menu, 'open-state-changed',
|
||||
shellItem, MenuItemFactory._onOpenStateChanged);
|
||||
}
|
||||
|
||||
return shellItem;
|
||||
},
|
||||
|
||||
_onOpenStateChanged(menu, open) {
|
||||
if (open) {
|
||||
if (NEED_NESTED_SUBMENU_FIX) {
|
||||
// close our own submenus
|
||||
if (menu._openedSubMenu)
|
||||
menu._openedSubMenu.close(false);
|
||||
|
||||
// register ourselves and close sibling submenus
|
||||
if (menu._parent._openedSubMenu && menu._parent._openedSubMenu !== menu)
|
||||
menu._parent._openedSubMenu.close(true);
|
||||
|
||||
menu._parent._openedSubMenu = menu;
|
||||
}
|
||||
|
||||
this._dbusItem.handleEvent('opened', null, 0);
|
||||
this._dbusItem.sendAboutToShow();
|
||||
} else {
|
||||
if (NEED_NESTED_SUBMENU_FIX) {
|
||||
// close our own submenus
|
||||
if (menu._openedSubMenu)
|
||||
menu._openedSubMenu.close(false);
|
||||
}
|
||||
|
||||
this._dbusItem.handleEvent('closed', null, 0);
|
||||
}
|
||||
},
|
||||
|
||||
_onActivate(_item, event) {
|
||||
const timestamp = event.get_time();
|
||||
if (timestamp && this._dbusClient.indicator)
|
||||
this._dbusClient.indicator.provideActivationToken(timestamp);
|
||||
|
||||
this._dbusItem.handleEvent('clicked', GLib.Variant.new('i', 0),
|
||||
timestamp);
|
||||
},
|
||||
|
||||
_onPropertyChanged(dbusItem, prop, _value) {
|
||||
if (prop === 'toggle-type' || prop === 'toggle-state')
|
||||
MenuItemFactory._updateOrnament.call(this);
|
||||
else if (prop === 'label')
|
||||
MenuItemFactory._updateLabel.call(this);
|
||||
else if (prop === 'enabled')
|
||||
MenuItemFactory._updateSensitive.call(this);
|
||||
else if (prop === 'visible')
|
||||
MenuItemFactory._updateVisible.call(this);
|
||||
else if (prop === 'icon-name' || prop === 'icon-data')
|
||||
MenuItemFactory._updateImage.call(this);
|
||||
else if (prop === 'type' || prop === 'children-display')
|
||||
MenuItemFactory._replaceSelf.call(this);
|
||||
else
|
||||
Util.Logger.debug(`Unhandled property change: ${prop}`);
|
||||
},
|
||||
|
||||
_onChildAdded(dbusItem, child, position) {
|
||||
if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) {
|
||||
Util.Logger.warn('Tried to add a child to non-submenu item. Better recreate it as whole');
|
||||
MenuItemFactory._replaceSelf.call(this);
|
||||
} else {
|
||||
this.menu.addMenuItem(MenuItemFactory.createItem(this._dbusClient, child), position);
|
||||
}
|
||||
},
|
||||
|
||||
_onChildRemoved(dbusItem, child) {
|
||||
if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) {
|
||||
Util.Logger.warn('Tried to remove a child from non-submenu item. Better recreate it as whole');
|
||||
MenuItemFactory._replaceSelf.call(this);
|
||||
} else {
|
||||
// find it!
|
||||
this.menu._getMenuItems().forEach(item => {
|
||||
if (item._dbusItem === child)
|
||||
item.destroy();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_onChildMoved(dbusItem, child, oldpos, newpos) {
|
||||
if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) {
|
||||
Util.Logger.warn('Tried to move a child in non-submenu item. Better recreate it as whole');
|
||||
MenuItemFactory._replaceSelf.call(this);
|
||||
} else {
|
||||
MenuUtils.moveItemInMenu(this.menu, child, newpos);
|
||||
}
|
||||
},
|
||||
|
||||
_updateLabel() {
|
||||
const label = this._dbusItem.propertyGet('label').replace(/_([^_])/, '$1');
|
||||
|
||||
if (this.label) // especially on GS3.8, the separator item might not even have a hidden label
|
||||
this.label.set_text(label);
|
||||
},
|
||||
|
||||
_updateOrnament() {
|
||||
if (!this.setOrnament)
|
||||
return; // separators and alike might not have gotten the polyfill
|
||||
|
||||
if (this._dbusItem.propertyGet('toggle-type') === 'checkmark' &&
|
||||
this._dbusItem.propertyGetInt('toggle-state'))
|
||||
this.setOrnament(PopupMenu.Ornament.CHECK);
|
||||
else if (this._dbusItem.propertyGet('toggle-type') === 'radio' &&
|
||||
this._dbusItem.propertyGetInt('toggle-state'))
|
||||
this.setOrnament(PopupMenu.Ornament.DOT);
|
||||
else
|
||||
this.setOrnament(PopupMenu.Ornament.NONE);
|
||||
},
|
||||
|
||||
async _updateImage() {
|
||||
if (!this._icon)
|
||||
return; // might be missing on submenus / separators
|
||||
|
||||
const iconName = this._dbusItem.propertyGet('icon-name');
|
||||
const iconData = this._dbusItem.propertyGetVariant('icon-data');
|
||||
if (iconName) {
|
||||
this._icon.icon_name = iconName;
|
||||
} else if (iconData) {
|
||||
try {
|
||||
const inputStream = Gio.MemoryInputStream.new_from_bytes(
|
||||
iconData.get_data_as_bytes());
|
||||
this._icon.gicon = await GdkPixbuf.Pixbuf.new_from_stream_async(
|
||||
inputStream, this._dbusClient.cancellable);
|
||||
} catch (e) {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_updateVisible() {
|
||||
this.visible = this._dbusItem.propertyGetBool('visible');
|
||||
},
|
||||
|
||||
_updateSensitive() {
|
||||
this.setSensitive(this._dbusItem.propertyGetBool('enabled'));
|
||||
},
|
||||
|
||||
_replaceSelf(newSelf) {
|
||||
// create our new self if needed
|
||||
if (!newSelf)
|
||||
newSelf = MenuItemFactory.createItem(this._dbusClient, this._dbusItem);
|
||||
|
||||
// first, we need to find our old position
|
||||
let pos = -1;
|
||||
const family = this._parent._getMenuItems();
|
||||
for (let i = 0; i < family.length; ++i) {
|
||||
if (family[i] === this)
|
||||
pos = i;
|
||||
}
|
||||
|
||||
if (pos < 0)
|
||||
throw new Error("DBusMenu: can't replace non existing menu item");
|
||||
|
||||
|
||||
// add our new self while we're still alive
|
||||
this._parent.addMenuItem(newSelf, pos);
|
||||
|
||||
// now destroy our old self
|
||||
this.destroy();
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility functions not necessarily belonging into the item factory
|
||||
*/
|
||||
const MenuUtils = {
|
||||
moveItemInMenu(menu, dbusItem, newpos) {
|
||||
// HACK: we're really getting into the internals of the PopupMenu implementation
|
||||
|
||||
// First, find our wrapper. Children tend to lie. We do not trust the old positioning.
|
||||
const family = menu._getMenuItems();
|
||||
for (let i = 0; i < family.length; ++i) {
|
||||
if (family[i]._dbusItem === dbusItem) {
|
||||
// now, remove it
|
||||
menu.box.remove_child(family[i]);
|
||||
|
||||
// and add it again somewhere else
|
||||
if (newpos < family.length && family[newpos] !== family[i])
|
||||
menu.box.insert_child_below(family[i], family[newpos]);
|
||||
else
|
||||
menu.box.add(family[i]);
|
||||
|
||||
// skip the rest
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Processes DBus events, creates the menu items and handles the actions
|
||||
*
|
||||
* Something like a mini-god-object
|
||||
*/
|
||||
export class Client extends Signals.EventEmitter {
|
||||
constructor(busName, path, indicator) {
|
||||
super();
|
||||
|
||||
this._busName = busName;
|
||||
this._busPath = path;
|
||||
this._client = new DBusClient(busName, path);
|
||||
this._rootMenu = null; // the shell menu
|
||||
this._rootItem = null; // the DbusMenuItem for the root
|
||||
this.indicator = indicator;
|
||||
this.cancellable = new Util.CancellableChild(this.indicator.cancellable);
|
||||
|
||||
this._client.initAsync(this.cancellable).catch(e => {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e);
|
||||
});
|
||||
|
||||
Util.connectSmart(this._client, 'ready-changed', this,
|
||||
() => this.emit('ready-changed'));
|
||||
}
|
||||
|
||||
get isReady() {
|
||||
return this._client.isReady;
|
||||
}
|
||||
|
||||
// this will attach the client to an already existing menu that will be used as the root menu.
|
||||
// it will also connect the client to be automatically destroyed when the menu dies.
|
||||
attachToMenu(menu) {
|
||||
this._rootMenu = menu;
|
||||
this._rootItem = this._client.getRoot();
|
||||
this._itemsBeingAdded = new Set();
|
||||
|
||||
// cleanup: remove existing children (just in case)
|
||||
this._rootMenu.removeAll();
|
||||
|
||||
if (NEED_NESTED_SUBMENU_FIX)
|
||||
menu._setOpenedSubMenu = this._setOpenedSubmenu.bind(this);
|
||||
|
||||
// connect handlers
|
||||
Util.connectSmart(menu, 'open-state-changed', this, this._onMenuOpened);
|
||||
Util.connectSmart(menu, 'destroy', this, this.destroy);
|
||||
|
||||
Util.connectSmart(this._rootItem, 'child-added', this, this._onRootChildAdded);
|
||||
Util.connectSmart(this._rootItem, 'child-removed', this, this._onRootChildRemoved);
|
||||
Util.connectSmart(this._rootItem, 'child-moved', this, this._onRootChildMoved);
|
||||
|
||||
// Dropbox requires us to call AboutToShow(0) first
|
||||
this._rootItem.sendAboutToShow();
|
||||
|
||||
// fill the menu for the first time
|
||||
const children = this._rootItem.getChildren();
|
||||
children.forEach(child =>
|
||||
this._onRootChildAdded(this._rootItem, child));
|
||||
}
|
||||
|
||||
_setOpenedSubmenu(submenu) {
|
||||
if (!submenu)
|
||||
return;
|
||||
|
||||
if (submenu._parent !== this._rootMenu)
|
||||
return;
|
||||
|
||||
if (submenu === this._openedSubMenu)
|
||||
return;
|
||||
|
||||
if (this._openedSubMenu && this._openedSubMenu.isOpen)
|
||||
this._openedSubMenu.close(true);
|
||||
|
||||
this._openedSubMenu = submenu;
|
||||
}
|
||||
|
||||
_onRootChildAdded(dbusItem, child, position) {
|
||||
// Menu additions can be expensive, so let's do it in different chunks
|
||||
const basePriority = this.isOpen ? GLib.PRIORITY_DEFAULT : GLib.PRIORITY_LOW;
|
||||
const idlePromise = new PromiseUtils.IdlePromise(
|
||||
basePriority + this._itemsBeingAdded.size, this.cancellable);
|
||||
this._itemsBeingAdded.add(child);
|
||||
|
||||
idlePromise.then(() => {
|
||||
if (!this._itemsBeingAdded.has(child))
|
||||
return;
|
||||
|
||||
this._rootMenu.addMenuItem(
|
||||
MenuItemFactory.createItem(this, child), position);
|
||||
}).catch(e => {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e);
|
||||
}).finally(() => this._itemsBeingAdded.delete(child));
|
||||
}
|
||||
|
||||
_onRootChildRemoved(dbusItem, child) {
|
||||
// children like to play hide and seek
|
||||
// but we know how to find it for sure!
|
||||
const item = this._rootMenu._getMenuItems().find(it =>
|
||||
it._dbusItem === child);
|
||||
|
||||
if (item)
|
||||
item.destroy();
|
||||
else
|
||||
this._itemsBeingAdded.delete(child);
|
||||
}
|
||||
|
||||
_onRootChildMoved(dbusItem, child, oldpos, newpos) {
|
||||
MenuUtils.moveItemInMenu(this._rootMenu, dbusItem, newpos);
|
||||
}
|
||||
|
||||
_onMenuOpened(menu, state) {
|
||||
if (!this._rootItem)
|
||||
return;
|
||||
|
||||
this._client.active = state;
|
||||
|
||||
if (state) {
|
||||
if (this._openedSubMenu && this._openedSubMenu.isOpen)
|
||||
this._openedSubMenu.close();
|
||||
|
||||
this._rootItem.handleEvent('opened', null, 0);
|
||||
this._rootItem.sendAboutToShow();
|
||||
} else {
|
||||
this._rootItem.handleEvent('closed', null, 0);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.emit('destroy');
|
||||
|
||||
if (this._client)
|
||||
this._client.destroy();
|
||||
|
||||
this._client = null;
|
||||
this._rootItem = null;
|
||||
this._rootMenu = null;
|
||||
this.indicator = null;
|
||||
this._itemsBeingAdded = null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
|
||||
import {CancellableChild, Logger} from './util.js';
|
||||
|
||||
Gio._promisify(Gio.DBusProxy.prototype, 'init_async');
|
||||
|
||||
export const DBusProxy = GObject.registerClass({
|
||||
Signals: {'destroy': {}},
|
||||
}, class DBusProxy extends Gio.DBusProxy {
|
||||
static get TUPLE_VARIANT_TYPE() {
|
||||
if (!this._tupleVariantType)
|
||||
this._tupleVariantType = new GLib.VariantType('(v)');
|
||||
|
||||
return this._tupleVariantType;
|
||||
}
|
||||
|
||||
static destroy() {
|
||||
delete this._tupleType;
|
||||
}
|
||||
|
||||
_init(busName, objectPath, interfaceInfo, flags = Gio.DBusProxyFlags.NONE) {
|
||||
if (interfaceInfo.signals.length)
|
||||
Logger.warn('Avoid exposing signals to gjs!');
|
||||
|
||||
super._init({
|
||||
gConnection: Gio.DBus.session,
|
||||
gInterfaceName: interfaceInfo.name,
|
||||
gInterfaceInfo: interfaceInfo,
|
||||
gName: busName,
|
||||
gObjectPath: objectPath,
|
||||
gFlags: flags,
|
||||
});
|
||||
|
||||
this._signalIds = [];
|
||||
|
||||
if (!(flags & Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS)) {
|
||||
this._signalIds.push(this.connect('g-signal',
|
||||
(_proxy, ...args) => this._onSignal(...args)));
|
||||
}
|
||||
|
||||
this._signalIds.push(this.connect('notify::g-name-owner', () =>
|
||||
this._onNameOwnerChanged()));
|
||||
}
|
||||
|
||||
async initAsync(cancellable) {
|
||||
cancellable = new CancellableChild(cancellable);
|
||||
await this.init_async(GLib.PRIORITY_DEFAULT, cancellable);
|
||||
this._cancellable = cancellable;
|
||||
|
||||
this.gInterfaceInfo.methods.map(m => m.name).forEach(method =>
|
||||
this._ensureAsyncMethod(method));
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.emit('destroy');
|
||||
|
||||
this._signalIds.forEach(id => this.disconnect(id));
|
||||
|
||||
if (this._cancellable)
|
||||
this._cancellable.cancel();
|
||||
}
|
||||
|
||||
// This can be removed when we will have GNOME 43 as minimum version
|
||||
_ensureAsyncMethod(method) {
|
||||
if (this[`${method}Async`])
|
||||
return;
|
||||
|
||||
if (!this[`${method}Remote`])
|
||||
throw new Error(`Missing remote method '${method}'`);
|
||||
|
||||
this[`${method}Async`] = function (...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this[`${method}Remote`](...args, (ret, e) => {
|
||||
if (e)
|
||||
reject(e);
|
||||
else
|
||||
resolve(ret);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
_onSignal() {
|
||||
}
|
||||
|
||||
getProperty(propertyName, cancellable) {
|
||||
return this.gConnection.call(this.gName,
|
||||
this.gObjectPath, 'org.freedesktop.DBus.Properties', 'Get',
|
||||
GLib.Variant.new('(ss)', [this.gInterfaceName, propertyName]),
|
||||
DBusProxy.TUPLE_VARIANT_TYPE, Gio.DBusCallFlags.NONE, -1,
|
||||
cancellable);
|
||||
}
|
||||
|
||||
getProperties(cancellable) {
|
||||
return this.gConnection.call(this.gName,
|
||||
this.gObjectPath, 'org.freedesktop.DBus.Properties', 'GetAll',
|
||||
GLib.Variant.new('(s)', [this.gInterfaceName]),
|
||||
GLib.VariantType.new('(a{sv})'), Gio.DBusCallFlags.NONE, -1,
|
||||
cancellable);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,89 @@
|
|||
// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import * as Extension from 'resource:///org/gnome/shell/extensions/extension.js';
|
||||
|
||||
import * as StatusNotifierWatcher from './statusNotifierWatcher.js';
|
||||
import * as Interfaces from './interfaces.js';
|
||||
import * as TrayIconsManager from './trayIconsManager.js';
|
||||
import * as Util from './util.js';
|
||||
import {SettingsManager} from './settingsManager.js';
|
||||
|
||||
export default class DashToDockExtension extends Extension.Extension {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
Util.Logger.init(this);
|
||||
Interfaces.initialize(this);
|
||||
|
||||
this._isEnabled = false;
|
||||
this._statusNotifierWatcher = null;
|
||||
this._watchDog = new Util.NameWatcher(StatusNotifierWatcher.WATCHER_BUS_NAME);
|
||||
this._watchDog.connect('vanished', () => this._maybeEnableAfterNameAvailable());
|
||||
|
||||
// HACK: we want to leave the watchdog alive when disabling the extension,
|
||||
// but if we are being reloaded, we destroy it since it could be considered
|
||||
// a leak and spams our log, too.
|
||||
/* eslint-disable no-undef */
|
||||
if (typeof global['--appindicator-extension-on-reload'] === 'function')
|
||||
global['--appindicator-extension-on-reload']();
|
||||
|
||||
global['--appindicator-extension-on-reload'] = () => {
|
||||
Util.Logger.debug('Reload detected, destroying old watchdog');
|
||||
this._watchDog.destroy();
|
||||
this._watchDog = null;
|
||||
};
|
||||
/* eslint-enable no-undef */
|
||||
}
|
||||
|
||||
enable() {
|
||||
this._isEnabled = true;
|
||||
SettingsManager.initialize(this);
|
||||
Util.tryCleanupOldIndicators();
|
||||
this._maybeEnableAfterNameAvailable();
|
||||
TrayIconsManager.TrayIconsManager.initialize();
|
||||
}
|
||||
|
||||
disable() {
|
||||
this._isEnabled = false;
|
||||
TrayIconsManager.TrayIconsManager.destroy();
|
||||
|
||||
if (this._statusNotifierWatcher !== null) {
|
||||
this._statusNotifierWatcher.destroy();
|
||||
this._statusNotifierWatcher = null;
|
||||
}
|
||||
|
||||
SettingsManager.destroy();
|
||||
}
|
||||
|
||||
// FIXME: when entering/leaving the lock screen, the extension might be
|
||||
// enabled/disabled rapidly.
|
||||
// This will create very bad side effects in case we were not done unowning
|
||||
// the name while trying to own it again. Since g_bus_unown_name doesn't
|
||||
// fire any callback when it's done, we need to monitor the bus manually
|
||||
// to find out when the name vanished so we can reclaim it again.
|
||||
_maybeEnableAfterNameAvailable() {
|
||||
// by the time we get called whe might not be enabled
|
||||
if (!this._isEnabled || this._statusNotifierWatcher)
|
||||
return;
|
||||
|
||||
if (this._watchDog.nameAcquired && this._watchDog.nameOnBus)
|
||||
return;
|
||||
|
||||
this._statusNotifierWatcher = new StatusNotifierWatcher.StatusNotifierWatcher(
|
||||
this._watchDog);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import GLib from 'gi://GLib';
|
||||
import Gio from 'gi://Gio';
|
||||
|
||||
import * as PromiseUtils from './promiseUtils.js';
|
||||
import * as Util from './util.js';
|
||||
|
||||
// The icon cache caches icon objects in case they're reused shortly aftwerwards.
|
||||
// This is necessary for some indicators like skype which rapidly switch between serveral icons.
|
||||
// Without caching, the garbage collection would never be able to handle the amount of new icon data.
|
||||
// If the lifetime of an icon is over, the cache will destroy the icon. (!)
|
||||
// The presence of active icons will extend the lifetime.
|
||||
|
||||
const GC_INTERVAL = 100; // seconds
|
||||
const LIFETIME_TIMESPAN = 120; // seconds
|
||||
|
||||
// how to use: see IconCache.add, IconCache.get
|
||||
export class IconCache {
|
||||
constructor() {
|
||||
this._cache = new Map();
|
||||
this._activeIcons = Object.create(null);
|
||||
this._lifetime = new Map(); // we don't want to attach lifetime to the object
|
||||
}
|
||||
|
||||
add(id, icon) {
|
||||
if (!(icon instanceof Gio.Icon)) {
|
||||
Util.Logger.critical('IconCache: Only Gio.Icons are supported');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
Util.Logger.critical('IconCache: Invalid ID provided');
|
||||
return null;
|
||||
}
|
||||
|
||||
const oldIcon = this._cache.get(id);
|
||||
if (!oldIcon || !oldIcon.equals(icon)) {
|
||||
Util.Logger.debug(`IconCache: adding ${id}: ${icon}`);
|
||||
this._cache.set(id, icon);
|
||||
} else {
|
||||
icon = oldIcon;
|
||||
}
|
||||
|
||||
this._renewLifetime(id);
|
||||
this._checkGC();
|
||||
|
||||
return icon;
|
||||
}
|
||||
|
||||
updateActive(iconType, gicon, isActive) {
|
||||
if (!gicon)
|
||||
return;
|
||||
|
||||
const previousActive = this._activeIcons[iconType];
|
||||
|
||||
if (isActive && [...this._cache.values()].some(icon => icon === gicon))
|
||||
this._activeIcons[iconType] = gicon;
|
||||
else if (previousActive === gicon)
|
||||
delete this._activeIcons[iconType];
|
||||
else
|
||||
return;
|
||||
|
||||
if (previousActive) {
|
||||
this._cache.forEach((icon, id) => {
|
||||
if (icon === previousActive)
|
||||
this._renewLifetime(id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_remove(id) {
|
||||
Util.Logger.debug(`IconCache: removing ${id}`);
|
||||
|
||||
this._cache.delete(id);
|
||||
this._lifetime.delete(id);
|
||||
}
|
||||
|
||||
_renewLifetime(id) {
|
||||
this._lifetime.set(id, new Date().getTime() + LIFETIME_TIMESPAN * 1000);
|
||||
}
|
||||
|
||||
forceDestroy(id) {
|
||||
const gicon = this._cache.has(id);
|
||||
if (gicon) {
|
||||
Object.keys(this._activeIcons).forEach(iconType =>
|
||||
this.updateActive(iconType, gicon, false));
|
||||
this._remove(id);
|
||||
this._checkGC();
|
||||
}
|
||||
}
|
||||
|
||||
// marks all the icons as removable, if something doesn't claim them before
|
||||
weakClear() {
|
||||
this._activeIcons = Object.create(null);
|
||||
this._checkGC();
|
||||
}
|
||||
|
||||
// removes everything from the cache
|
||||
clear() {
|
||||
this._activeIcons = Object.create(null);
|
||||
this._cache.forEach((_icon, id) => this._remove(id));
|
||||
this._checkGC();
|
||||
}
|
||||
|
||||
// returns an object from the cache, or null if it can't be found.
|
||||
get(id) {
|
||||
const icon = this._cache.get(id);
|
||||
if (icon) {
|
||||
Util.Logger.debug(`IconCache: retrieving ${id}: ${icon}`);
|
||||
this._renewLifetime(id);
|
||||
return icon;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async _checkGC() {
|
||||
const cacheIsEmpty = this._cache.size === 0;
|
||||
|
||||
if (!cacheIsEmpty && !this._gcTimeout) {
|
||||
Util.Logger.debug('IconCache: garbage collector started');
|
||||
let anyUnusedInCache = false;
|
||||
this._gcTimeout = new PromiseUtils.TimeoutSecondsPromise(GC_INTERVAL,
|
||||
GLib.PRIORITY_LOW);
|
||||
try {
|
||||
await this._gcTimeout;
|
||||
anyUnusedInCache = this._gc();
|
||||
} catch (e) {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e, 'IconCache: garbage collector');
|
||||
} finally {
|
||||
delete this._gcTimeout;
|
||||
}
|
||||
|
||||
if (anyUnusedInCache)
|
||||
this._checkGC();
|
||||
} else if (cacheIsEmpty && this._gcTimeout) {
|
||||
Util.Logger.debug('IconCache: garbage collector stopped');
|
||||
this._gcTimeout.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
_gc() {
|
||||
const time = new Date().getTime();
|
||||
let anyUnused = false;
|
||||
|
||||
this._cache.forEach((icon, id) => {
|
||||
if (Object.values(this._activeIcons).includes(icon)) {
|
||||
Util.Logger.debug(`IconCache: ${id} is in use.`);
|
||||
} else if (this._lifetime.get(id) < time) {
|
||||
this._remove(id);
|
||||
} else {
|
||||
anyUnused = true;
|
||||
Util.Logger.debug(`IconCache: ${id} survived this round.`);
|
||||
}
|
||||
});
|
||||
|
||||
return anyUnused;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.clear();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,592 @@
|
|||
// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import Clutter from 'gi://Clutter';
|
||||
import Gio from 'gi://Gio';
|
||||
import GObject from 'gi://GObject';
|
||||
import St from 'gi://St';
|
||||
|
||||
import * as AppDisplay from 'resource:///org/gnome/shell/ui/appDisplay.js';
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
import * as Panel from 'resource:///org/gnome/shell/ui/panel.js';
|
||||
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
|
||||
|
||||
import * as AppIndicator from './appIndicator.js';
|
||||
import * as PromiseUtils from './promiseUtils.js';
|
||||
import * as SettingsManager from './settingsManager.js';
|
||||
import * as Util from './util.js';
|
||||
import * as DBusMenu from './dbusMenu.js';
|
||||
|
||||
const DEFAULT_ICON_SIZE = Panel.PANEL_ICON_SIZE || 16;
|
||||
|
||||
export function addIconToPanel(statusIcon) {
|
||||
if (!(statusIcon instanceof BaseStatusIcon))
|
||||
throw TypeError(`Unexpected icon type: ${statusIcon}`);
|
||||
|
||||
const settings = SettingsManager.getDefaultGSettings();
|
||||
const indicatorId = `appindicator-${statusIcon.uniqueId}`;
|
||||
|
||||
const currentIcon = Main.panel.statusArea[indicatorId];
|
||||
if (currentIcon) {
|
||||
if (currentIcon !== statusIcon)
|
||||
currentIcon.destroy();
|
||||
|
||||
Main.panel.statusArea[indicatorId] = null;
|
||||
}
|
||||
|
||||
Main.panel.addToStatusArea(indicatorId, statusIcon, 1,
|
||||
settings.get_string('tray-pos'));
|
||||
|
||||
Util.connectSmart(settings, 'changed::tray-pos', statusIcon, () =>
|
||||
addIconToPanel(statusIcon));
|
||||
}
|
||||
|
||||
export function getTrayIcons() {
|
||||
return Object.values(Main.panel.statusArea).filter(
|
||||
i => i instanceof IndicatorStatusTrayIcon);
|
||||
}
|
||||
|
||||
export function getAppIndicatorIcons() {
|
||||
return Object.values(Main.panel.statusArea).filter(
|
||||
i => i instanceof IndicatorStatusIcon);
|
||||
}
|
||||
|
||||
export const BaseStatusIcon = GObject.registerClass(
|
||||
class IndicatorBaseStatusIcon extends PanelMenu.Button {
|
||||
_init(menuAlignment, nameText, iconActor, dontCreateMenu) {
|
||||
super._init(menuAlignment, nameText, dontCreateMenu);
|
||||
|
||||
const settings = SettingsManager.getDefaultGSettings();
|
||||
Util.connectSmart(settings, 'changed::icon-opacity', this, this._updateOpacity);
|
||||
this.connect('notify::hover', () => this._onHoverChanged());
|
||||
|
||||
if (!super._onDestroy)
|
||||
this.connect('destroy', () => this._onDestroy());
|
||||
|
||||
this._box = new St.BoxLayout({style_class: 'panel-status-indicators-box'});
|
||||
this.add_child(this._box);
|
||||
|
||||
this._setIconActor(iconActor);
|
||||
this._showIfReady();
|
||||
}
|
||||
|
||||
_setIconActor(icon) {
|
||||
if (!(icon instanceof Clutter.Actor))
|
||||
throw new Error(`${icon} is not a valid actor`);
|
||||
|
||||
if (this._icon && this._icon !== icon)
|
||||
this._icon.destroy();
|
||||
|
||||
this._icon = icon;
|
||||
this._updateEffects();
|
||||
this._monitorIconEffects();
|
||||
|
||||
if (this._icon) {
|
||||
this._box.add_child(this._icon);
|
||||
const id = this._icon.connect('destroy', () => {
|
||||
this._icon.disconnect(id);
|
||||
this._icon = null;
|
||||
this._monitorIconEffects();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onDestroy() {
|
||||
if (this._icon)
|
||||
this._icon.destroy();
|
||||
|
||||
if (super._onDestroy)
|
||||
super._onDestroy();
|
||||
}
|
||||
|
||||
isReady() {
|
||||
throw new GObject.NotImplementedError('isReady() in %s'.format(this.constructor.name));
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return this._icon;
|
||||
}
|
||||
|
||||
get uniqueId() {
|
||||
throw new GObject.NotImplementedError('uniqueId in %s'.format(this.constructor.name));
|
||||
}
|
||||
|
||||
_showIfReady() {
|
||||
this.visible = this.isReady();
|
||||
}
|
||||
|
||||
_onHoverChanged() {
|
||||
if (this.hover) {
|
||||
this.opacity = 255;
|
||||
if (this._icon)
|
||||
this._icon.remove_effect_by_name('desaturate');
|
||||
} else {
|
||||
this._updateEffects();
|
||||
}
|
||||
}
|
||||
|
||||
_updateOpacity() {
|
||||
const settings = SettingsManager.getDefaultGSettings();
|
||||
const userValue = settings.get_user_value('icon-opacity');
|
||||
if (userValue)
|
||||
this.opacity = userValue.unpack();
|
||||
else
|
||||
this.opacity = 255;
|
||||
}
|
||||
|
||||
_updateEffects() {
|
||||
this._updateOpacity();
|
||||
|
||||
if (this._icon) {
|
||||
this._updateSaturation();
|
||||
this._updateBrightnessContrast();
|
||||
}
|
||||
}
|
||||
|
||||
_monitorIconEffects() {
|
||||
const settings = SettingsManager.getDefaultGSettings();
|
||||
const monitoring = !!this._iconSaturationIds;
|
||||
|
||||
if (!this._icon && monitoring) {
|
||||
Util.disconnectSmart(settings, this, this._iconSaturationIds);
|
||||
delete this._iconSaturationIds;
|
||||
|
||||
Util.disconnectSmart(settings, this, this._iconBrightnessIds);
|
||||
delete this._iconBrightnessIds;
|
||||
|
||||
Util.disconnectSmart(settings, this, this._iconContrastIds);
|
||||
delete this._iconContrastIds;
|
||||
} else if (this._icon && !monitoring) {
|
||||
this._iconSaturationIds =
|
||||
Util.connectSmart(settings, 'changed::icon-saturation', this,
|
||||
this._updateSaturation);
|
||||
this._iconBrightnessIds =
|
||||
Util.connectSmart(settings, 'changed::icon-brightness', this,
|
||||
this._updateBrightnessContrast);
|
||||
this._iconContrastIds =
|
||||
Util.connectSmart(settings, 'changed::icon-contrast', this,
|
||||
this._updateBrightnessContrast);
|
||||
}
|
||||
}
|
||||
|
||||
_updateSaturation() {
|
||||
const settings = SettingsManager.getDefaultGSettings();
|
||||
const desaturationValue = settings.get_double('icon-saturation');
|
||||
let desaturateEffect = this._icon.get_effect('desaturate');
|
||||
|
||||
if (desaturationValue > 0) {
|
||||
if (!desaturateEffect) {
|
||||
desaturateEffect = new Clutter.DesaturateEffect();
|
||||
this._icon.add_effect_with_name('desaturate', desaturateEffect);
|
||||
}
|
||||
desaturateEffect.set_factor(desaturationValue);
|
||||
} else if (desaturateEffect) {
|
||||
this._icon.remove_effect(desaturateEffect);
|
||||
}
|
||||
}
|
||||
|
||||
_updateBrightnessContrast() {
|
||||
const settings = SettingsManager.getDefaultGSettings();
|
||||
const brightnessValue = settings.get_double('icon-brightness');
|
||||
const contrastValue = settings.get_double('icon-contrast');
|
||||
let brightnessContrastEffect = this._icon.get_effect('brightness-contrast');
|
||||
|
||||
if (brightnessValue !== 0 | contrastValue !== 0) {
|
||||
if (!brightnessContrastEffect) {
|
||||
brightnessContrastEffect = new Clutter.BrightnessContrastEffect();
|
||||
this._icon.add_effect_with_name('brightness-contrast', brightnessContrastEffect);
|
||||
}
|
||||
brightnessContrastEffect.set_brightness(brightnessValue);
|
||||
brightnessContrastEffect.set_contrast(contrastValue);
|
||||
} else if (brightnessContrastEffect) {
|
||||
this._icon.remove_effect(brightnessContrastEffect);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* IndicatorStatusIcon implements an icon in the system status area
|
||||
*/
|
||||
export const IndicatorStatusIcon = GObject.registerClass(
|
||||
class IndicatorStatusIcon extends BaseStatusIcon {
|
||||
_init(indicator) {
|
||||
super._init(0.5, indicator.accessibleName,
|
||||
new AppIndicator.IconActor(indicator, DEFAULT_ICON_SIZE));
|
||||
this._indicator = indicator;
|
||||
|
||||
this._lastClickTime = -1;
|
||||
this._lastClickX = -1;
|
||||
this._lastClickY = -1;
|
||||
|
||||
this._box.add_style_class_name('appindicator-box');
|
||||
|
||||
Util.connectSmart(this._indicator, 'ready', this, this._showIfReady);
|
||||
Util.connectSmart(this._indicator, 'menu', this, this._updateMenu);
|
||||
Util.connectSmart(this._indicator, 'label', this, this._updateLabel);
|
||||
Util.connectSmart(this._indicator, 'status', this, this._updateStatus);
|
||||
Util.connectSmart(this._indicator, 'reset', this, () => {
|
||||
this._updateStatus();
|
||||
this._updateLabel();
|
||||
});
|
||||
Util.connectSmart(this._indicator, 'accessible-name', this, () =>
|
||||
this.set_accessible_name(this._indicator.accessibleName));
|
||||
Util.connectSmart(this._indicator, 'destroy', this, () => this.destroy());
|
||||
|
||||
this.connect('notify::visible', () => this._updateMenu());
|
||||
|
||||
this._showIfReady();
|
||||
}
|
||||
|
||||
_onDestroy() {
|
||||
if (this._menuClient) {
|
||||
this._menuClient.disconnect(this._menuReadyId);
|
||||
this._menuClient.destroy();
|
||||
this._menuClient = null;
|
||||
}
|
||||
|
||||
super._onDestroy();
|
||||
}
|
||||
|
||||
get uniqueId() {
|
||||
return this._indicator.uniqueId;
|
||||
}
|
||||
|
||||
isReady() {
|
||||
return this._indicator && this._indicator.isReady;
|
||||
}
|
||||
|
||||
_updateLabel() {
|
||||
const {label} = this._indicator;
|
||||
if (label) {
|
||||
if (!this._label || !this._labelBin) {
|
||||
this._labelBin = new St.Bin({
|
||||
yAlign: Clutter.ActorAlign.CENTER,
|
||||
});
|
||||
this._label = new St.Label();
|
||||
this._labelBin.add_actor(this._label);
|
||||
this._box.add_actor(this._labelBin);
|
||||
}
|
||||
this._label.set_text(label);
|
||||
if (!this._box.contains(this._labelBin))
|
||||
this._box.add_actor(this._labelBin); // FIXME: why is it suddenly necessary?
|
||||
} else if (this._label) {
|
||||
this._labelBin.destroy_all_children();
|
||||
this._box.remove_actor(this._labelBin);
|
||||
this._labelBin.destroy();
|
||||
delete this._labelBin;
|
||||
delete this._label;
|
||||
}
|
||||
}
|
||||
|
||||
_updateStatus() {
|
||||
const wasVisible = this.visible;
|
||||
this.visible = this._indicator.status !== AppIndicator.SNIStatus.PASSIVE;
|
||||
|
||||
if (this.visible !== wasVisible)
|
||||
this._indicator.checkAlive().catch(logError);
|
||||
}
|
||||
|
||||
_updateMenu() {
|
||||
if (this._menuClient) {
|
||||
this._menuClient.disconnect(this._menuReadyId);
|
||||
this._menuClient.destroy();
|
||||
this._menuClient = null;
|
||||
this.menu.removeAll();
|
||||
}
|
||||
|
||||
if (this.visible && this._indicator.menuPath) {
|
||||
this._menuClient = new DBusMenu.Client(this._indicator.busName,
|
||||
this._indicator.menuPath, this._indicator);
|
||||
|
||||
if (this._menuClient.isReady)
|
||||
this._menuClient.attachToMenu(this.menu);
|
||||
|
||||
this._menuReadyId = this._menuClient.connect('ready-changed', () => {
|
||||
if (this._menuClient.isReady)
|
||||
this._menuClient.attachToMenu(this.menu);
|
||||
else
|
||||
this._updateMenu();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_showIfReady() {
|
||||
if (!this.isReady())
|
||||
return;
|
||||
|
||||
this._updateLabel();
|
||||
this._updateStatus();
|
||||
this._updateMenu();
|
||||
}
|
||||
|
||||
_updateClickCount(event) {
|
||||
const [x, y] = event.get_coords();
|
||||
const time = event.get_time();
|
||||
const {doubleClickDistance, doubleClickTime} =
|
||||
Clutter.Settings.get_default();
|
||||
|
||||
if (time > (this._lastClickTime + doubleClickTime) ||
|
||||
(Math.abs(x - this._lastClickX) > doubleClickDistance) ||
|
||||
(Math.abs(y - this._lastClickY) > doubleClickDistance))
|
||||
this._clickCount = 0;
|
||||
|
||||
this._lastClickTime = time;
|
||||
this._lastClickX = x;
|
||||
this._lastClickY = y;
|
||||
|
||||
this._clickCount = (this._clickCount % 2) + 1;
|
||||
|
||||
return this._clickCount;
|
||||
}
|
||||
|
||||
_maybeHandleDoubleClick(event) {
|
||||
if (this._indicator.supportsActivation === false)
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
|
||||
if (event.get_button() !== Clutter.BUTTON_PRIMARY)
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
|
||||
if (this._updateClickCount(event) === 2) {
|
||||
this._indicator.open(...event.get_coords(), event.get_time());
|
||||
return Clutter.EVENT_STOP;
|
||||
}
|
||||
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
}
|
||||
|
||||
async _waitForDoubleClick() {
|
||||
const {doubleClickTime} = Clutter.Settings.get_default();
|
||||
this._waitDoubleClickPromise = new PromiseUtils.TimeoutPromise(
|
||||
doubleClickTime);
|
||||
|
||||
try {
|
||||
await this._waitDoubleClickPromise;
|
||||
this.menu.toggle();
|
||||
} catch (e) {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
throw e;
|
||||
} finally {
|
||||
delete this._waitDoubleClickPromise;
|
||||
}
|
||||
}
|
||||
|
||||
vfunc_event(event) {
|
||||
if (this.menu.numMenuItems && event.type() === Clutter.EventType.TOUCH_BEGIN)
|
||||
this.menu.toggle();
|
||||
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
}
|
||||
|
||||
vfunc_button_press_event(event) {
|
||||
if (this._waitDoubleClickPromise)
|
||||
this._waitDoubleClickPromise.cancel();
|
||||
|
||||
// if middle mouse button clicked send SecondaryActivate dbus event and do not show appindicator menu
|
||||
if (event.get_button() === Clutter.BUTTON_MIDDLE) {
|
||||
if (Main.panel.menuManager.activeMenu)
|
||||
Main.panel.menuManager._closeMenu(true, Main.panel.menuManager.activeMenu);
|
||||
this._indicator.secondaryActivate(event.get_time(), ...event.get_coords());
|
||||
return Clutter.EVENT_STOP;
|
||||
}
|
||||
|
||||
if (event.get_button() === Clutter.BUTTON_SECONDARY) {
|
||||
this.menu.toggle();
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
}
|
||||
|
||||
const doubleClickHandled = this._maybeHandleDoubleClick(event);
|
||||
if (doubleClickHandled === Clutter.EVENT_PROPAGATE &&
|
||||
event.get_button() === Clutter.BUTTON_PRIMARY &&
|
||||
this.menu.numMenuItems) {
|
||||
if (this._indicator.supportsActivation !== false)
|
||||
this._waitForDoubleClick().catch(logError);
|
||||
else
|
||||
this.menu.toggle();
|
||||
}
|
||||
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
}
|
||||
|
||||
vfunc_button_release_event(event) {
|
||||
if (!this._indicator.supportsActivation)
|
||||
return this._maybeHandleDoubleClick(event);
|
||||
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
}
|
||||
|
||||
vfunc_scroll_event(event) {
|
||||
// Since Clutter 1.10, clutter will always send a smooth scrolling event
|
||||
// with explicit deltas, no matter what input device is used
|
||||
// In fact, for every scroll there will be a smooth and non-smooth scroll
|
||||
// event, and we can choose which one we interpret.
|
||||
if (event.get_scroll_direction() === Clutter.ScrollDirection.SMOOTH) {
|
||||
const [dx, dy] = event.get_scroll_delta();
|
||||
|
||||
this._indicator.scroll(dx, dy);
|
||||
return Clutter.EVENT_STOP;
|
||||
}
|
||||
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
}
|
||||
});
|
||||
|
||||
export const IndicatorStatusTrayIcon = GObject.registerClass(
|
||||
class IndicatorTrayIcon extends BaseStatusIcon {
|
||||
_init(icon) {
|
||||
super._init(0.5, icon.wm_class, icon, {dontCreateMenu: true});
|
||||
Util.Logger.debug(`Adding legacy tray icon ${this.uniqueId}`);
|
||||
this._box.add_style_class_name('appindicator-trayicons-box');
|
||||
this.add_style_class_name('appindicator-icon');
|
||||
this.add_style_class_name('tray-icon');
|
||||
|
||||
this.connect('button-press-event', (_actor, _event) => {
|
||||
this.add_style_pseudo_class('active');
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
});
|
||||
this.connect('button-release-event', (_actor, event) => {
|
||||
this._icon.click(event);
|
||||
this.remove_style_pseudo_class('active');
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
});
|
||||
this.connect('key-press-event', (_actor, event) => {
|
||||
this.add_style_pseudo_class('active');
|
||||
this._icon.click(event);
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
});
|
||||
this.connect('key-release-event', (_actor, event) => {
|
||||
this._icon.click(event);
|
||||
this.remove_style_pseudo_class('active');
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
});
|
||||
|
||||
Util.connectSmart(this._icon, 'destroy', this, () => {
|
||||
icon.clear_effects();
|
||||
this.destroy();
|
||||
});
|
||||
|
||||
const settings = SettingsManager.getDefaultGSettings();
|
||||
Util.connectSmart(settings, 'changed::icon-size', this, this._updateIconSize);
|
||||
|
||||
const themeContext = St.ThemeContext.get_for_stage(global.stage);
|
||||
Util.connectSmart(themeContext, 'notify::scale-factor', this, () =>
|
||||
this._updateIconSize());
|
||||
|
||||
this._updateIconSize();
|
||||
}
|
||||
|
||||
_onDestroy() {
|
||||
Util.Logger.debug(`Destroying legacy tray icon ${this.uniqueId}`);
|
||||
|
||||
if (this._waitDoubleClickPromise)
|
||||
this._waitDoubleClickPromise.cancel();
|
||||
|
||||
super._onDestroy();
|
||||
}
|
||||
|
||||
isReady() {
|
||||
return !!this._icon;
|
||||
}
|
||||
|
||||
get uniqueId() {
|
||||
return `legacy:${this._icon.wm_class}:${this._icon.pid}`;
|
||||
}
|
||||
|
||||
vfunc_navigate_focus(from, direction) {
|
||||
this.grab_key_focus();
|
||||
return super.vfunc_navigate_focus(from, direction);
|
||||
}
|
||||
|
||||
_getSimulatedButtonEvent(touchEvent) {
|
||||
const event = Clutter.Event.new(Clutter.EventType.BUTTON_RELEASE);
|
||||
event.set_button(1);
|
||||
event.set_time(touchEvent.get_time());
|
||||
event.set_flags(touchEvent.get_flags());
|
||||
event.set_stage(global.stage);
|
||||
event.set_source(touchEvent.get_source());
|
||||
event.set_coords(...touchEvent.get_coords());
|
||||
event.set_state(touchEvent.get_state());
|
||||
return event;
|
||||
}
|
||||
|
||||
vfunc_touch_event(event) {
|
||||
// Under X11 we rely on emulated pointer events
|
||||
if (!imports.gi.Meta.is_wayland_compositor())
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
|
||||
const slot = event.get_event_sequence().get_slot();
|
||||
|
||||
if (!this._touchPressSlot &&
|
||||
event.get_type() === Clutter.EventType.TOUCH_BEGIN) {
|
||||
this.add_style_pseudo_class('active');
|
||||
this._touchButtonEvent = this._getSimulatedButtonEvent(event);
|
||||
this._touchPressSlot = slot;
|
||||
this._touchDelayPromise = new PromiseUtils.TimeoutPromise(
|
||||
AppDisplay.MENU_POPUP_TIMEOUT);
|
||||
this._touchDelayPromise.then(() => {
|
||||
delete this._touchDelayPromise;
|
||||
delete this._touchPressSlot;
|
||||
this._touchButtonEvent.set_button(3);
|
||||
this._icon.click(this._touchButtonEvent);
|
||||
this.remove_style_pseudo_class('active');
|
||||
});
|
||||
} else if (event.get_type() === Clutter.EventType.TOUCH_END &&
|
||||
this._touchPressSlot === slot) {
|
||||
delete this._touchPressSlot;
|
||||
delete this._touchButtonEvent;
|
||||
if (this._touchDelayPromise) {
|
||||
this._touchDelayPromise.cancel();
|
||||
delete this._touchDelayPromise;
|
||||
}
|
||||
|
||||
this._icon.click(this._getSimulatedButtonEvent(event));
|
||||
this.remove_style_pseudo_class('active');
|
||||
} else if (event.get_type() === Clutter.EventType.TOUCH_UPDATE &&
|
||||
this._touchPressSlot === slot) {
|
||||
this.add_style_pseudo_class('active');
|
||||
this._touchButtonEvent = this._getSimulatedButtonEvent(event);
|
||||
}
|
||||
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
}
|
||||
|
||||
vfunc_leave_event(event) {
|
||||
this.remove_style_pseudo_class('active');
|
||||
|
||||
if (this._touchDelayPromise) {
|
||||
this._touchDelayPromise.cancel();
|
||||
delete this._touchDelayPromise;
|
||||
}
|
||||
|
||||
return super.vfunc_leave_event(event);
|
||||
}
|
||||
|
||||
_updateIconSize() {
|
||||
const settings = SettingsManager.getDefaultGSettings();
|
||||
const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);
|
||||
let iconSize = settings.get_int('icon-size');
|
||||
|
||||
if (iconSize <= 0)
|
||||
iconSize = DEFAULT_ICON_SIZE;
|
||||
|
||||
this.height = -1;
|
||||
this._icon.set({
|
||||
width: iconSize * scaleFactor,
|
||||
height: iconSize * scaleFactor,
|
||||
xAlign: Clutter.ActorAlign.CENTER,
|
||||
yAlign: Clutter.ActorAlign.CENTER,
|
||||
});
|
||||
}
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
<interface name="com.canonical.dbusmenu">
|
||||
<!-- Properties -->
|
||||
<property name="Version" type="u" access="read" />
|
||||
<property name="TextDirection" type="s" access="read" />
|
||||
<property name="Status" type="s" access="read" />
|
||||
<property name="IconThemePath" type="as" access="read" />
|
||||
|
||||
<!-- Functions -->
|
||||
<method name="GetLayout">
|
||||
<arg type="i" name="parentId" direction="in" />
|
||||
<arg type="i" name="recursionDepth" direction="in" />
|
||||
<arg type="as" name="propertyNames" direction="in" />
|
||||
<arg type="u" name="revision" direction="out" />
|
||||
<arg type="(ia{sv}av)" name="layout" direction="out" />
|
||||
</method>
|
||||
|
||||
<method name="GetGroupProperties">
|
||||
<arg type="ai" name="ids" direction="in" />
|
||||
<arg type="as" name="propertyNames" direction="in" />
|
||||
<arg type="a(ia{sv})" name="properties" direction="out" />
|
||||
</method>
|
||||
|
||||
<method name="GetProperty">
|
||||
<arg type="i" name="id" direction="in" />
|
||||
<arg type="s" name="name" direction="in" />
|
||||
<arg type="v" name="value" direction="out" />
|
||||
</method>
|
||||
|
||||
<method name="Event">
|
||||
<arg type="i" name="id" direction="in" />
|
||||
<arg type="s" name="eventId" direction="in" />
|
||||
<arg type="v" name="data" direction="in" />
|
||||
<arg type="u" name="timestamp" direction="in" />
|
||||
</method>
|
||||
|
||||
<method name="EventGroup">
|
||||
<arg type="a(isvu)" name="events" direction="in" />
|
||||
<arg type="ai" name="idErrors" direction="out" />
|
||||
</method>
|
||||
|
||||
<method name="AboutToShow">
|
||||
<arg type="i" name="id" direction="in" />
|
||||
<arg type="b" name="needUpdate" direction="out" />
|
||||
</method>
|
||||
|
||||
<method name="AboutToShowGroup">
|
||||
<arg type="ai" name="ids" direction="in" />
|
||||
<arg type="ai" name="updatesNeeded" direction="out" />
|
||||
<arg type="ai" name="idErrors" direction="out" />
|
||||
</method>
|
||||
|
||||
<!-- Signals
|
||||
<signal name="ItemsPropertiesUpdated">
|
||||
<arg type="a(ia{sv})" name="updatedProps" direction="out" />
|
||||
<arg type="a(ias)" name="removedProps" direction="out" />
|
||||
</signal>
|
||||
<signal name="LayoutUpdated">
|
||||
<arg type="u" name="revision" direction="out" />
|
||||
<arg type="i" name="parent" direction="out" />
|
||||
</signal>
|
||||
<signal name="ItemActivationRequested">
|
||||
<arg type="i" name="id" direction="out" />
|
||||
<arg type="u" name="timestamp" direction="out" />
|
||||
</signal>
|
||||
-->
|
||||
</interface>
|
|
@ -0,0 +1,130 @@
|
|||
<!-- Based on:
|
||||
https://invent.kde.org/frameworks/knotifications/-/blob/master/src/org.kde.StatusNotifierItem.xml
|
||||
-->
|
||||
|
||||
<interface name="org.kde.StatusNotifierItem">
|
||||
|
||||
<property name="Category" type="s" access="read"/>
|
||||
<property name="Id" type="s" access="read"/>
|
||||
<property name="Title" type="s" access="read"/>
|
||||
<property name="Status" type="s" access="read"/>
|
||||
<property name="WindowId" type="i" access="read"/>
|
||||
|
||||
<!-- An additional path to add to the theme search path to find the icons specified above. -->
|
||||
<property name="IconThemePath" type="s" access="read"/>
|
||||
<property name="Menu" type="o" access="read"/>
|
||||
<property name="ItemIsMenu" type="b" access="read"/>
|
||||
|
||||
|
||||
<!-- main icon -->
|
||||
<!-- names are preferred over pixmaps -->
|
||||
<property name="IconName" type="s" access="read"/>
|
||||
|
||||
<!--struct containing width, height and image data-->
|
||||
<property name="IconPixmap" type="a(iiay)" access="read">
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName" value="KDbusImageVector"/>
|
||||
</property>
|
||||
|
||||
<property name="OverlayIconName" type="s" access="read"/>
|
||||
|
||||
<property name="OverlayIconPixmap" type="a(iiay)" access="read">
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName" value="KDbusImageVector"/>
|
||||
</property>
|
||||
|
||||
|
||||
<!-- Requesting attention icon -->
|
||||
<property name="AttentionIconName" type="s" access="read"/>
|
||||
|
||||
<!--same definition as image-->
|
||||
<property name="AttentionIconPixmap" type="a(iiay)" access="read">
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName" value="KDbusImageVector"/>
|
||||
</property>
|
||||
|
||||
<property name="AttentionMovieName" type="s" access="read"/>
|
||||
|
||||
|
||||
|
||||
<!-- tooltip data -->
|
||||
|
||||
<!--(iiay) is an image-->
|
||||
<!-- We disable this as we don't support tooltip, so no need to go through it
|
||||
<property name="ToolTip" type="(sa(iiay)ss)" access="read">
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName" value="KDbusToolTipStruct"/>
|
||||
</property>
|
||||
-->
|
||||
|
||||
<!-- interaction: the systemtray wants the application to do something -->
|
||||
<method name="ContextMenu">
|
||||
<!-- we're passing the coordinates of the icon, so the app knows where to put the popup window -->
|
||||
<arg name="x" type="i" direction="in"/>
|
||||
<arg name="y" type="i" direction="in"/>
|
||||
</method>
|
||||
|
||||
<method name="Activate">
|
||||
<arg name="x" type="i" direction="in"/>
|
||||
<arg name="y" type="i" direction="in"/>
|
||||
</method>
|
||||
|
||||
<method name="ProvideXdgActivationToken">
|
||||
<arg name="token" type="s" direction="in"/>
|
||||
</method>
|
||||
|
||||
<method name="SecondaryActivate">
|
||||
<arg name="x" type="i" direction="in"/>
|
||||
<arg name="y" type="i" direction="in"/>
|
||||
</method>
|
||||
|
||||
<method name="XAyatanaSecondaryActivate">
|
||||
<arg name="timestamp" type="u" direction="in"/>
|
||||
</method>
|
||||
|
||||
<method name="Scroll">
|
||||
<arg name="delta" type="i" direction="in"/>
|
||||
<arg name="orientation" type="s" direction="in"/>
|
||||
</method>
|
||||
|
||||
<!-- Signals: the client wants to change something in the status
|
||||
<signal name="NewTitle">
|
||||
</signal>
|
||||
|
||||
<signal name="NewIcon">
|
||||
</signal>
|
||||
|
||||
<signal name="NewAttentionIcon">
|
||||
</signal>
|
||||
|
||||
<signal name="NewOverlayIcon">
|
||||
</signal>
|
||||
-->
|
||||
<!-- We disable this as we don't support tooltip, so no need to go through it
|
||||
<signal name="NewToolTip">
|
||||
</signal>
|
||||
-->
|
||||
|
||||
<!--
|
||||
<signal name="NewStatus">
|
||||
<arg name="status" type="s"/>
|
||||
</signal>
|
||||
-->
|
||||
|
||||
|
||||
<!-- The following items are not supported by specs, but widely used
|
||||
<signal name="NewIconThemePath">
|
||||
<arg type="s" name="icon_theme_path" direction="out" />
|
||||
</signal>
|
||||
|
||||
<signal name="NewMenu"></signal>
|
||||
-->
|
||||
|
||||
<!-- ayatana labels -->
|
||||
<!-- These are commented out because GDBusProxy would otherwise require them,
|
||||
but they are not available for KDE indicators
|
||||
-->
|
||||
<!--<signal name="XAyatanaNewLabel">
|
||||
<arg type="s" name="label" direction="out" />
|
||||
<arg type="s" name="guide" direction="out" />
|
||||
</signal>
|
||||
<property name="XAyatanaLabel" type="s" access="read" />
|
||||
<property name="XAyatanaLabelGuide" type="s" access="read" />-->
|
||||
|
||||
</interface>
|
|
@ -0,0 +1,38 @@
|
|||
<interface name="org.kde.StatusNotifierWatcher">
|
||||
|
||||
<!-- methods -->
|
||||
<method name="RegisterStatusNotifierItem">
|
||||
<arg name="service" type="s" direction="in"/>
|
||||
</method>
|
||||
|
||||
<method name="RegisterStatusNotifierHost">
|
||||
<arg name="service" type="s" direction="in"/>
|
||||
</method>
|
||||
|
||||
|
||||
<!-- properties -->
|
||||
|
||||
<property name="RegisteredStatusNotifierItems" type="as" access="read">
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QStringList"/>
|
||||
</property>
|
||||
|
||||
<property name="IsStatusNotifierHostRegistered" type="b" access="read"/>
|
||||
|
||||
<property name="ProtocolVersion" type="i" access="read"/>
|
||||
|
||||
|
||||
<!-- signals -->
|
||||
<signal name="StatusNotifierItemRegistered">
|
||||
<arg type="s"/>
|
||||
</signal>
|
||||
|
||||
<signal name="StatusNotifierItemUnregistered">
|
||||
<arg type="s"/>
|
||||
</signal>
|
||||
|
||||
<signal name="StatusNotifierHostRegistered">
|
||||
</signal>
|
||||
|
||||
<signal name="StatusNotifierHostUnregistered">
|
||||
</signal>
|
||||
</interface>
|
|
@ -0,0 +1,52 @@
|
|||
// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
export let StatusNotifierItem = null;
|
||||
export let StatusNotifierWatcher = null;
|
||||
export let DBusMenu = null;
|
||||
|
||||
// loads a xml file into an in-memory string
|
||||
function loadInterfaceXml(extension, filename) {
|
||||
const interfacesDir = extension.dir.get_child('interfaces-xml');
|
||||
const file = interfacesDir.get_child(filename);
|
||||
const [result, contents] = imports.gi.GLib.file_get_contents(file.get_path());
|
||||
|
||||
if (result) {
|
||||
// HACK: The "" + trick is important as hell because file_get_contents returns
|
||||
// an object (WTF?) but Gio.makeProxyWrapper requires `typeof() === "string"`
|
||||
// Otherwise, it will try to check `instanceof XML` and fail miserably because there
|
||||
// is no `XML` on very recent SpiderMonkey releases (or, if SpiderMonkey is old enough,
|
||||
// will spit out a TypeError soon).
|
||||
let nodeContents = contents;
|
||||
if (contents instanceof Uint8Array)
|
||||
nodeContents = imports.byteArray.toString(contents);
|
||||
return `<node>${nodeContents}</node>`;
|
||||
} else {
|
||||
throw new Error(`AppIndicatorSupport: Could not load file: ${filename}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function initialize(extension) {
|
||||
StatusNotifierItem = loadInterfaceXml(extension, 'StatusNotifierItem.xml');
|
||||
StatusNotifierWatcher = loadInterfaceXml(extension, 'StatusNotifierWatcher.xml');
|
||||
DBusMenu = loadInterfaceXml(extension, 'DBusMenu.xml');
|
||||
}
|
||||
|
||||
export function destroy() {
|
||||
StatusNotifierItem = null;
|
||||
StatusNotifierWatcher = null;
|
||||
DBusMenu = null;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"_generated": "Generated by SweetTooth, do not edit",
|
||||
"description": "Adds AppIndicator, KStatusNotifierItem and legacy Tray icons support to the Shell",
|
||||
"gettext-domain": "AppIndicatorExtension",
|
||||
"name": "AppIndicator and KStatusNotifierItem Support",
|
||||
"settings-schema": "org.gnome.shell.extensions.appindicator",
|
||||
"shell-version": [
|
||||
"45"
|
||||
],
|
||||
"url": "https://github.com/ubuntu/gnome-shell-extension-appindicator",
|
||||
"uuid": "appindicatorsupport@rgcjonas.gmail.com",
|
||||
"version": 57
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
export function argbToRgba(src) {
|
||||
const dest = new Uint8Array(src.length);
|
||||
|
||||
for (let j = 0; j < src.length; j += 4) {
|
||||
const srcAlpha = src[j];
|
||||
|
||||
dest[j] = src[j + 1]; /* red */
|
||||
dest[j + 1] = src[j + 2]; /* green */
|
||||
dest[j + 2] = src[j + 3]; /* blue */
|
||||
dest[j + 3] = srcAlpha; /* alpha */
|
||||
}
|
||||
|
||||
return dest;
|
||||
}
|
||||
|
||||
export function getBestPixmap(pixmapsVariant, preferredSize) {
|
||||
if (!pixmapsVariant)
|
||||
throw new TypeError('null pixmapsVariant');
|
||||
|
||||
const pixmapsVariantsArray = new Array(pixmapsVariant.n_children());
|
||||
if (!pixmapsVariantsArray.length)
|
||||
throw TypeError('Empty Icon found');
|
||||
|
||||
for (let i = 0; i < pixmapsVariantsArray.length; ++i)
|
||||
pixmapsVariantsArray[i] = pixmapsVariant.get_child_value(i);
|
||||
|
||||
const pixmapsSizedArray = pixmapsVariantsArray.map((pixmapVariant, index) => ({
|
||||
width: pixmapVariant.get_child_value(0).unpack(),
|
||||
height: pixmapVariant.get_child_value(1).unpack(),
|
||||
index,
|
||||
}));
|
||||
|
||||
const sortedIconPixmapArray = pixmapsSizedArray.sort(
|
||||
({width: widthA, height: heightA}, {width: widthB, height: heightB}) => {
|
||||
const areaA = widthA * heightA;
|
||||
const areaB = widthB * heightB;
|
||||
|
||||
return areaA - areaB;
|
||||
});
|
||||
|
||||
// we prefer any pixmap that is equal or bigger than our requested size
|
||||
const qualifiedIconPixmapArray = sortedIconPixmapArray.filter(({width, height}) =>
|
||||
width >= preferredSize && height >= preferredSize);
|
||||
|
||||
const {width, height, index} = qualifiedIconPixmapArray.length > 0
|
||||
? qualifiedIconPixmapArray[0] : sortedIconPixmapArray.pop();
|
||||
|
||||
const pixmapVariant = pixmapsVariantsArray[index].get_child_value(2);
|
||||
const rowStride = width * 4; // hopefully this is correct
|
||||
|
||||
return {pixmapVariant, width, height, rowStride};
|
||||
}
|
|
@ -0,0 +1,326 @@
|
|||
// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*-
|
||||
|
||||
/* exported init, buildPrefsWidget */
|
||||
|
||||
import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
import Gio from 'gi://Gio';
|
||||
import Gtk from 'gi://Gtk';
|
||||
|
||||
import {
|
||||
ExtensionPreferences,
|
||||
gettext as _
|
||||
} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
|
||||
|
||||
const AppIndicatorPreferences = GObject.registerClass(
|
||||
class AppIndicatorPreferences extends Gtk.Box {
|
||||
_init(extension) {
|
||||
super._init({orientation: Gtk.Orientation.VERTICAL, spacing: 30});
|
||||
this._settings = extension.getSettings();
|
||||
|
||||
let label = null;
|
||||
let widget = null;
|
||||
|
||||
this.preferences_vbox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
spacing: 8,
|
||||
margin_start: 30,
|
||||
margin_end: 30,
|
||||
margin_top: 30,
|
||||
margin_bottom: 30,
|
||||
});
|
||||
this.custom_icons_vbox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
spacing: 10,
|
||||
margin_start: 10,
|
||||
margin_end: 10,
|
||||
margin_top: 10,
|
||||
margin_bottom: 10,
|
||||
});
|
||||
|
||||
label = new Gtk.Label({
|
||||
label: _('Enable Legacy Tray Icons support'),
|
||||
hexpand: true,
|
||||
halign: Gtk.Align.START,
|
||||
});
|
||||
widget = new Gtk.Switch({halign: Gtk.Align.END});
|
||||
|
||||
this._settings.bind('legacy-tray-enabled', widget, 'active',
|
||||
Gio.SettingsBindFlags.DEFAULT);
|
||||
|
||||
this.legacy_tray_hbox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
spacing: 10,
|
||||
margin_start: 10,
|
||||
margin_end: 10,
|
||||
margin_top: 10,
|
||||
margin_bottom: 10,
|
||||
});
|
||||
|
||||
this.legacy_tray_hbox.append(label);
|
||||
this.legacy_tray_hbox.append(widget);
|
||||
|
||||
// Icon opacity
|
||||
this.opacity_hbox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
spacing: 10,
|
||||
margin_start: 10,
|
||||
margin_end: 10,
|
||||
margin_top: 10,
|
||||
margin_bottom: 10,
|
||||
});
|
||||
label = new Gtk.Label({
|
||||
label: _('Opacity (min: 0, max: 255)'),
|
||||
hexpand: true,
|
||||
halign: Gtk.Align.START,
|
||||
});
|
||||
|
||||
widget = new Gtk.SpinButton({halign: Gtk.Align.END});
|
||||
widget.set_sensitive(true);
|
||||
widget.set_range(0, 255);
|
||||
widget.set_value(this._settings.get_int('icon-opacity'));
|
||||
widget.set_increments(1, 2);
|
||||
widget.connect('value-changed', w => {
|
||||
this._settings.set_int('icon-opacity', w.get_value_as_int());
|
||||
});
|
||||
this.opacity_hbox.append(label);
|
||||
this.opacity_hbox.append(widget);
|
||||
|
||||
// Icon saturation
|
||||
this.saturation_hbox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
spacing: 10,
|
||||
margin_start: 10,
|
||||
margin_end: 10,
|
||||
margin_top: 10,
|
||||
margin_bottom: 10,
|
||||
});
|
||||
label = new Gtk.Label({
|
||||
label: _('Desaturation (min: 0.0, max: 1.0)'),
|
||||
hexpand: true,
|
||||
halign: Gtk.Align.START,
|
||||
});
|
||||
widget = new Gtk.SpinButton({halign: Gtk.Align.END, digits: 1});
|
||||
widget.set_sensitive(true);
|
||||
widget.set_range(0.0, 1.0);
|
||||
widget.set_value(this._settings.get_double('icon-saturation'));
|
||||
widget.set_increments(0.1, 0.2);
|
||||
widget.connect('value-changed', w => {
|
||||
this._settings.set_double('icon-saturation', w.get_value());
|
||||
});
|
||||
this.saturation_hbox.append(label);
|
||||
this.saturation_hbox.append(widget);
|
||||
|
||||
// Icon brightness
|
||||
this.brightness_hbox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
spacing: 10,
|
||||
margin_start: 10,
|
||||
margin_end: 10,
|
||||
margin_top: 10,
|
||||
margin_bottom: 10,
|
||||
});
|
||||
label = new Gtk.Label({
|
||||
label: _('Brightness (min: -1.0, max: 1.0)'),
|
||||
hexpand: true,
|
||||
halign: Gtk.Align.START,
|
||||
});
|
||||
widget = new Gtk.SpinButton({halign: Gtk.Align.END, digits: 1});
|
||||
widget.set_sensitive(true);
|
||||
widget.set_range(-1.0, 1.0);
|
||||
widget.set_value(this._settings.get_double('icon-brightness'));
|
||||
widget.set_increments(0.1, 0.2);
|
||||
widget.connect('value-changed', w => {
|
||||
this._settings.set_double('icon-brightness', w.get_value());
|
||||
});
|
||||
this.brightness_hbox.append(label);
|
||||
this.brightness_hbox.append(widget);
|
||||
|
||||
// Icon contrast
|
||||
this.contrast_hbox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
spacing: 10,
|
||||
margin_start: 10,
|
||||
margin_end: 10,
|
||||
margin_top: 10,
|
||||
margin_bottom: 10,
|
||||
});
|
||||
label = new Gtk.Label({
|
||||
label: _('Contrast (min: -1.0, max: 1.0)'),
|
||||
hexpand: true,
|
||||
halign: Gtk.Align.START,
|
||||
});
|
||||
widget = new Gtk.SpinButton({halign: Gtk.Align.END, digits: 1});
|
||||
widget.set_sensitive(true);
|
||||
widget.set_range(-1.0, 1.0);
|
||||
widget.set_value(this._settings.get_double('icon-contrast'));
|
||||
widget.set_increments(0.1, 0.2);
|
||||
widget.connect('value-changed', w => {
|
||||
this._settings.set_double('icon-contrast', w.get_value());
|
||||
});
|
||||
this.contrast_hbox.append(label);
|
||||
this.contrast_hbox.append(widget);
|
||||
|
||||
// Icon size
|
||||
this.icon_size_hbox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
spacing: 10,
|
||||
margin_start: 10,
|
||||
margin_end: 10,
|
||||
margin_top: 10,
|
||||
margin_bottom: 10,
|
||||
});
|
||||
label = new Gtk.Label({
|
||||
label: _('Icon size (min: 0, max: 96)'),
|
||||
hexpand: true,
|
||||
halign: Gtk.Align.START,
|
||||
});
|
||||
widget = new Gtk.SpinButton({halign: Gtk.Align.END});
|
||||
widget.set_sensitive(true);
|
||||
widget.set_range(0, 96);
|
||||
widget.set_value(this._settings.get_int('icon-size'));
|
||||
widget.set_increments(1, 2);
|
||||
widget.connect('value-changed', w => {
|
||||
this._settings.set_int('icon-size', w.get_value_as_int());
|
||||
});
|
||||
this.icon_size_hbox.append(label);
|
||||
this.icon_size_hbox.append(widget);
|
||||
|
||||
// Tray position in panel
|
||||
this.tray_position_hbox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
spacing: 10,
|
||||
margin_start: 10,
|
||||
margin_end: 10,
|
||||
margin_top: 10,
|
||||
margin_bottom: 10,
|
||||
});
|
||||
label = new Gtk.Label({
|
||||
label: _('Tray horizontal alignment'),
|
||||
hexpand: true,
|
||||
halign: Gtk.Align.START,
|
||||
});
|
||||
widget = new Gtk.ComboBoxText();
|
||||
widget.append('center', _('Center'));
|
||||
widget.append('left', _('Left'));
|
||||
widget.append('right', _('Right'));
|
||||
this._settings.bind('tray-pos', widget, 'active-id',
|
||||
Gio.SettingsBindFlags.DEFAULT);
|
||||
this.tray_position_hbox.append(label);
|
||||
this.tray_position_hbox.append(widget);
|
||||
|
||||
this.preferences_vbox.append(this.legacy_tray_hbox);
|
||||
this.preferences_vbox.append(this.opacity_hbox);
|
||||
this.preferences_vbox.append(this.saturation_hbox);
|
||||
this.preferences_vbox.append(this.brightness_hbox);
|
||||
this.preferences_vbox.append(this.contrast_hbox);
|
||||
this.preferences_vbox.append(this.icon_size_hbox);
|
||||
this.preferences_vbox.append(this.tray_position_hbox);
|
||||
|
||||
// Custom icons section
|
||||
|
||||
const customListStore = new Gtk.ListStore();
|
||||
customListStore.set_column_types([
|
||||
GObject.TYPE_STRING,
|
||||
GObject.TYPE_STRING,
|
||||
GObject.TYPE_STRING,
|
||||
]);
|
||||
const customInitArray = this._settings.get_value('custom-icons').deep_unpack();
|
||||
customInitArray.forEach(pair => {
|
||||
customListStore.set(customListStore.append(), [0, 1, 2], pair);
|
||||
});
|
||||
customListStore.append();
|
||||
|
||||
const customTreeView = new Gtk.TreeView({
|
||||
model: customListStore,
|
||||
hexpand: true,
|
||||
vexpand: true,
|
||||
});
|
||||
const customTitles = [
|
||||
_('Indicator ID'),
|
||||
_('Icon Name'),
|
||||
_('Attention Icon Name'),
|
||||
];
|
||||
const indicatorIdColumn = new Gtk.TreeViewColumn({
|
||||
title: customTitles[0],
|
||||
sizing: Gtk.TreeViewColumnSizing.AUTOSIZE,
|
||||
});
|
||||
const customIconColumn = new Gtk.TreeViewColumn({
|
||||
title: customTitles[1],
|
||||
sizing: Gtk.TreeViewColumnSizing.AUTOSIZE,
|
||||
});
|
||||
const customAttentionIconColumn = new Gtk.TreeViewColumn({
|
||||
title: customTitles[2],
|
||||
sizing: Gtk.TreeViewColumnSizing.AUTOSIZE,
|
||||
});
|
||||
|
||||
const cellrenderer = new Gtk.CellRendererText({editable: true});
|
||||
|
||||
indicatorIdColumn.pack_start(cellrenderer, true);
|
||||
customIconColumn.pack_start(cellrenderer, true);
|
||||
customAttentionIconColumn.pack_start(cellrenderer, true);
|
||||
indicatorIdColumn.add_attribute(cellrenderer, 'text', 0);
|
||||
customIconColumn.add_attribute(cellrenderer, 'text', 1);
|
||||
customAttentionIconColumn.add_attribute(cellrenderer, 'text', 2);
|
||||
customTreeView.insert_column(indicatorIdColumn, 0);
|
||||
customTreeView.insert_column(customIconColumn, 1);
|
||||
customTreeView.insert_column(customAttentionIconColumn, 2);
|
||||
customTreeView.set_grid_lines(Gtk.TreeViewGridLines.BOTH);
|
||||
|
||||
this.custom_icons_vbox.append(customTreeView);
|
||||
|
||||
cellrenderer.connect('edited', (w, path, text) => {
|
||||
this.selection = customTreeView.get_selection();
|
||||
const title = customTreeView.get_cursor()[1].get_title();
|
||||
const columnIndex = customTitles.indexOf(title);
|
||||
const selection = this.selection.get_selected();
|
||||
const iter = selection.at(2);
|
||||
const text2 = customListStore.get_value(iter, columnIndex ? 0 : 1);
|
||||
customListStore.set(iter, [columnIndex], [text]);
|
||||
const storeLength = customListStore.iter_n_children(null);
|
||||
const customIconArray = [];
|
||||
|
||||
for (let i = 0; i < storeLength; i++) {
|
||||
const returnIter = customListStore.iter_nth_child(null, i);
|
||||
const [success, iterList] = returnIter;
|
||||
if (!success)
|
||||
break;
|
||||
|
||||
if (iterList) {
|
||||
const id = customListStore.get_value(iterList, 0);
|
||||
const customIcon = customListStore.get_value(iterList, 1);
|
||||
const customAttentionIcon = customListStore.get_value(iterList, 2);
|
||||
if (id && customIcon)
|
||||
customIconArray.push([id, customIcon, customAttentionIcon || '']);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._settings.set_value('custom-icons', new GLib.Variant(
|
||||
'a(sss)', customIconArray));
|
||||
if (storeLength === 1 && (text || text2))
|
||||
customListStore.append();
|
||||
|
||||
if (storeLength > 1) {
|
||||
if ((!text && !text2) && (storeLength - 1 > path))
|
||||
customListStore.remove(iter);
|
||||
if ((text || text2) && storeLength - 1 <= path)
|
||||
customListStore.append();
|
||||
}
|
||||
});
|
||||
|
||||
this.notebook = new Gtk.Notebook();
|
||||
this.notebook.append_page(this.preferences_vbox,
|
||||
new Gtk.Label({label: _('Preferences')}));
|
||||
this.notebook.append_page(this.custom_icons_vbox,
|
||||
new Gtk.Label({label: _('Custom Icons')}));
|
||||
|
||||
this.append(this.notebook);
|
||||
}
|
||||
});
|
||||
|
||||
export default class DockPreferences extends ExtensionPreferences {
|
||||
getPreferencesWidget() {
|
||||
return new AppIndicatorPreferences(this);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,324 @@
|
|||
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
||||
|
||||
import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
import Meta from 'gi://GdkPixbuf';
|
||||
|
||||
import * as Signals from 'resource:///org/gnome/shell/misc/signals.js';
|
||||
|
||||
export class CancellablePromise extends Promise {
|
||||
constructor(executor, cancellable) {
|
||||
if (!(executor instanceof Function))
|
||||
throw TypeError('executor is not a function');
|
||||
|
||||
if (cancellable && !(cancellable instanceof Gio.Cancellable))
|
||||
throw TypeError('cancellable parameter is not a Gio.Cancellable');
|
||||
|
||||
let rejector;
|
||||
let resolver;
|
||||
super((resolve, reject) => {
|
||||
resolver = resolve;
|
||||
rejector = reject;
|
||||
});
|
||||
|
||||
const {stack: promiseStack} = new Error();
|
||||
this._promiseStack = promiseStack;
|
||||
|
||||
this._resolver = (...args) => {
|
||||
resolver(...args);
|
||||
this._resolved = true;
|
||||
this._cleanup();
|
||||
};
|
||||
this._rejector = (...args) => {
|
||||
rejector(...args);
|
||||
this._rejected = true;
|
||||
this._cleanup();
|
||||
};
|
||||
|
||||
if (!cancellable) {
|
||||
executor(this._resolver, this._rejector);
|
||||
return;
|
||||
}
|
||||
|
||||
this._cancellable = cancellable;
|
||||
this._cancelled = cancellable.is_cancelled();
|
||||
if (this._cancelled) {
|
||||
this._rejector(new GLib.Error(Gio.IOErrorEnum,
|
||||
Gio.IOErrorEnum.CANCELLED, 'Promise cancelled'));
|
||||
return;
|
||||
}
|
||||
|
||||
this._cancellationId = cancellable.connect(() => {
|
||||
const id = this._cancellationId;
|
||||
this._cancellationId = 0;
|
||||
GLib.idle_add(GLib.PRIORITY_DEFAULT, () => cancellable.disconnect(id));
|
||||
this.cancel();
|
||||
});
|
||||
|
||||
executor(this._resolver, this._rejector);
|
||||
}
|
||||
|
||||
_cleanup() {
|
||||
if (this._cancellationId)
|
||||
this._cancellable.disconnect(this._cancellationId);
|
||||
}
|
||||
|
||||
get cancellable() {
|
||||
return this._chainRoot._cancellable || null;
|
||||
}
|
||||
|
||||
get _chainRoot() {
|
||||
return this._root ? this._root : this;
|
||||
}
|
||||
|
||||
then(...args) {
|
||||
const ret = super.then(...args);
|
||||
|
||||
/* Every time we call then() on this promise we'd get a new
|
||||
* CancellablePromise however that won't have the properties that the
|
||||
* root one has set, and then it won't be possible to cancel a promise
|
||||
* chain from the last one.
|
||||
* To allow this we keep track of the root promise, make sure that
|
||||
* the same method on the root object is called during cancellation
|
||||
* or any destruction method if you want this to work. */
|
||||
if (ret instanceof CancellablePromise)
|
||||
ret._root = this._chainRoot;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
resolved() {
|
||||
return !!this._chainRoot._resolved;
|
||||
}
|
||||
|
||||
rejected() {
|
||||
return !!this._chainRoot._rejected;
|
||||
}
|
||||
|
||||
cancelled() {
|
||||
return !!this._chainRoot._cancelled;
|
||||
}
|
||||
|
||||
pending() {
|
||||
return !this.resolved() && !this.rejected();
|
||||
}
|
||||
|
||||
cancel() {
|
||||
if (this._root) {
|
||||
this._root.cancel();
|
||||
return this;
|
||||
}
|
||||
|
||||
if (!this.pending())
|
||||
return this;
|
||||
|
||||
this._cancelled = true;
|
||||
const error = new GLib.Error(Gio.IOErrorEnum,
|
||||
Gio.IOErrorEnum.CANCELLED, 'Promise cancelled');
|
||||
error.stack += `## Promise created at:\n${this._promiseStack}`;
|
||||
this._rejector(error);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class SignalConnectionPromise extends CancellablePromise {
|
||||
constructor(object, signal, cancellable) {
|
||||
if (arguments.length === 1 && object instanceof Function) {
|
||||
super(object);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(object.connect instanceof Function))
|
||||
throw new TypeError('Not a valid object');
|
||||
|
||||
if (object instanceof GObject.Object &&
|
||||
!GObject.signal_lookup(signal.split(':')[0], object.constructor.$gtype))
|
||||
throw new TypeError(`Signal ${signal} not found on object ${object}`);
|
||||
|
||||
let id;
|
||||
let destroyId;
|
||||
super(resolve => {
|
||||
let connectSignal;
|
||||
if (object instanceof GObject.Object)
|
||||
connectSignal = (sig, cb) => GObject.signal_connect(object, sig, cb);
|
||||
else
|
||||
connectSignal = (sig, cb) => object.connect(sig, cb);
|
||||
|
||||
id = connectSignal(signal, (_obj, ...args) => {
|
||||
if (!args.length)
|
||||
resolve();
|
||||
else
|
||||
resolve(args.length === 1 ? args[0] : args);
|
||||
});
|
||||
|
||||
if (signal !== 'destroy' &&
|
||||
(!(object instanceof GObject.Object) ||
|
||||
GObject.signal_lookup('destroy', object.constructor.$gtype)))
|
||||
destroyId = connectSignal('destroy', () => this.cancel());
|
||||
}, cancellable);
|
||||
|
||||
this._object = object;
|
||||
this._id = id;
|
||||
this._destroyId = destroyId;
|
||||
}
|
||||
|
||||
_cleanup() {
|
||||
if (this._id) {
|
||||
let disconnectSignal;
|
||||
|
||||
if (this._object instanceof GObject.Object)
|
||||
disconnectSignal = id => GObject.signal_handler_disconnect(this._object, id);
|
||||
else
|
||||
disconnectSignal = id => this._object.disconnect(id);
|
||||
|
||||
disconnectSignal(this._id);
|
||||
if (this._destroyId) {
|
||||
disconnectSignal(this._destroyId);
|
||||
this._destroyId = 0;
|
||||
}
|
||||
this._object = null;
|
||||
this._id = 0;
|
||||
}
|
||||
|
||||
super._cleanup();
|
||||
}
|
||||
|
||||
get object() {
|
||||
return this._chainRoot._object;
|
||||
}
|
||||
}
|
||||
|
||||
export class GSourcePromise extends CancellablePromise {
|
||||
constructor(gsource, priority, cancellable) {
|
||||
if (arguments.length === 1 && gsource instanceof Function) {
|
||||
super(gsource);
|
||||
return;
|
||||
}
|
||||
|
||||
if (gsource.constructor.$gtype !== GLib.Source.$gtype)
|
||||
throw new TypeError(`gsource ${gsource} is not of type GLib.Source`);
|
||||
|
||||
if (priority === undefined)
|
||||
priority = GLib.PRIORITY_DEFAULT;
|
||||
else if (!Number.isInteger(priority))
|
||||
throw TypeError('Invalid priority');
|
||||
|
||||
super(resolve => {
|
||||
gsource.set_priority(priority);
|
||||
gsource.set_callback(() => {
|
||||
resolve();
|
||||
return GLib.SOURCE_REMOVE;
|
||||
});
|
||||
gsource.attach(null);
|
||||
}, cancellable);
|
||||
|
||||
this._gsource = gsource;
|
||||
this._gsource.set_name(`[gnome-shell] ${this.constructor.name} ${
|
||||
new Error().stack.split('\n').filter(line =>
|
||||
!line.match(/misc\/promiseUtils\.js/))[0]}`);
|
||||
|
||||
if (this.rejected())
|
||||
this._gsource.destroy();
|
||||
}
|
||||
|
||||
get gsource() {
|
||||
return this._chainRoot._gsource;
|
||||
}
|
||||
|
||||
_cleanup() {
|
||||
if (this._gsource) {
|
||||
this._gsource.destroy();
|
||||
this._gsource = null;
|
||||
}
|
||||
super._cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
export class IdlePromise extends GSourcePromise {
|
||||
constructor(priority, cancellable) {
|
||||
if (arguments.length === 1 && priority instanceof Function) {
|
||||
super(priority);
|
||||
return;
|
||||
}
|
||||
|
||||
if (priority === undefined)
|
||||
priority = GLib.PRIORITY_DEFAULT_IDLE;
|
||||
|
||||
super(GLib.idle_source_new(), priority, cancellable);
|
||||
}
|
||||
}
|
||||
|
||||
export class TimeoutPromise extends GSourcePromise {
|
||||
constructor(interval, priority, cancellable) {
|
||||
if (arguments.length === 1 && interval instanceof Function) {
|
||||
super(interval);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Number.isInteger(interval) || interval < 0)
|
||||
throw TypeError('Invalid interval');
|
||||
|
||||
super(GLib.timeout_source_new(interval), priority, cancellable);
|
||||
}
|
||||
}
|
||||
|
||||
export class TimeoutSecondsPromise extends GSourcePromise {
|
||||
constructor(interval, priority, cancellable) {
|
||||
if (arguments.length === 1 && interval instanceof Function) {
|
||||
super(interval);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Number.isInteger(interval) || interval < 0)
|
||||
throw TypeError('Invalid interval');
|
||||
|
||||
super(GLib.timeout_source_new_seconds(interval), priority, cancellable);
|
||||
}
|
||||
}
|
||||
|
||||
export class MetaLaterPromise extends CancellablePromise {
|
||||
constructor(laterType, cancellable) {
|
||||
if (arguments.length === 1 && laterType instanceof Function) {
|
||||
super(laterType);
|
||||
return;
|
||||
}
|
||||
|
||||
if (laterType && laterType.constructor.$gtype !== Meta.LaterType.$gtype)
|
||||
throw new TypeError(`laterType ${laterType} is not of type Meta.LaterType`);
|
||||
else if (!laterType)
|
||||
laterType = Meta.LaterType.BEFORE_REDRAW;
|
||||
|
||||
let id;
|
||||
super(resolve => {
|
||||
id = Meta.later_add(laterType, () => {
|
||||
this.remove();
|
||||
resolve();
|
||||
return GLib.SOURCE_REMOVE;
|
||||
});
|
||||
}, cancellable);
|
||||
|
||||
this._id = id;
|
||||
}
|
||||
|
||||
_cleanup() {
|
||||
if (this._id) {
|
||||
Meta.later_remove(this._id);
|
||||
this._id = 0;
|
||||
}
|
||||
super._cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
export function _promisifySignals(proto) {
|
||||
if (proto.connect_once)
|
||||
return;
|
||||
|
||||
proto.connect_once = function (signal, cancellable) {
|
||||
return new SignalConnectionPromise(this, signal, cancellable);
|
||||
};
|
||||
}
|
||||
|
||||
_promisifySignals(GObject.Object.prototype);
|
||||
_promisifySignals(Signals.EventEmitter.prototype);
|
|
@ -0,0 +1,49 @@
|
|||
<schemalist gettext-domain="AppIndicatorExtension">
|
||||
<schema id="org.gnome.shell.extensions.appindicator" path="/org/gnome/shell/extensions/appindicator/">
|
||||
<key name="legacy-tray-enabled" type="b">
|
||||
<default>true</default>
|
||||
<summary>Enable legacy tray icons support</summary>
|
||||
</key>
|
||||
<key name="icon-saturation" type="d">
|
||||
<default>0.0</default>
|
||||
<summary>Saturation</summary>
|
||||
</key>
|
||||
<key name="icon-brightness" type="d">
|
||||
<default>0.0</default>
|
||||
<summary>Brightness</summary>
|
||||
</key>
|
||||
<key name="icon-contrast" type="d">
|
||||
<default>0.0</default>
|
||||
<summary>Contrast</summary>
|
||||
</key>
|
||||
<key name="icon-opacity" type="i">
|
||||
<default>240</default>
|
||||
<summary>Opacity</summary>
|
||||
</key>
|
||||
<key name="icon-size" type="i">
|
||||
<default>0</default>
|
||||
<summary>Icon size</summary>
|
||||
<description>Icon size in pixel</description>
|
||||
</key>
|
||||
<key name="icon-spacing" type="i">
|
||||
<default>12</default>
|
||||
<summary>Icon spacing</summary>
|
||||
<description>Icon spacing within the tray</description>
|
||||
</key>
|
||||
<key name="tray-pos" type="s">
|
||||
<default>"right"</default>
|
||||
<summary>Position in tray</summary>
|
||||
<description>Set where the Icon tray should appear in Gnome tray</description>
|
||||
</key>
|
||||
<key name="tray-order" type="i">
|
||||
<default>1</default>
|
||||
<summary>Order in tray</summary>
|
||||
<description>Set where the Icon tray should appear among other trays</description>
|
||||
</key>
|
||||
<key name="custom-icons" type="a(sss)">
|
||||
<default>[]</default>
|
||||
<summary>Custom icons</summary>
|
||||
<description>Replace any icons with custom icons from themes</description>
|
||||
</key>
|
||||
</schema>
|
||||
</schemalist>
|
|
@ -0,0 +1,55 @@
|
|||
// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
let settingsManager;
|
||||
|
||||
export class SettingsManager {
|
||||
static initialize(extension) {
|
||||
SettingsManager._settingsManager = new SettingsManager(extension);
|
||||
}
|
||||
|
||||
static destroy() {
|
||||
SettingsManager._settingsManager.destroy();
|
||||
SettingsManager._settingsManager = null;
|
||||
}
|
||||
|
||||
static getDefault() {
|
||||
return this._settingsManager;
|
||||
}
|
||||
|
||||
get gsettings() {
|
||||
return this._gsettings;
|
||||
}
|
||||
|
||||
constructor(extension) {
|
||||
if (settingsManager)
|
||||
throw new Error('SettingsManager is already constructed');
|
||||
|
||||
this._gsettings = extension.getSettings();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._gsettings = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getDefault() {
|
||||
return SettingsManager.getDefault();
|
||||
}
|
||||
|
||||
export function getDefaultGSettings() {
|
||||
return SettingsManager.getDefault().gsettings;
|
||||
}
|
|
@ -0,0 +1,287 @@
|
|||
// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
|
||||
import * as AppIndicator from './appIndicator.js';
|
||||
import * as IndicatorStatusIcon from './indicatorStatusIcon.js';
|
||||
import * as Interfaces from './interfaces.js';
|
||||
import * as PromiseUtils from './promiseUtils.js';
|
||||
import * as Util from './util.js';
|
||||
import * as DBusMenu from './dbusMenu.js';
|
||||
|
||||
import {DBusProxy} from './dbusProxy.js';
|
||||
|
||||
|
||||
// TODO: replace with org.freedesktop and /org/freedesktop when approved
|
||||
const KDE_PREFIX = 'org.kde';
|
||||
|
||||
export const WATCHER_BUS_NAME = `${KDE_PREFIX}.StatusNotifierWatcher`;
|
||||
const WATCHER_OBJECT = '/StatusNotifierWatcher';
|
||||
|
||||
const DEFAULT_ITEM_OBJECT_PATH = '/StatusNotifierItem';
|
||||
|
||||
/*
|
||||
* The StatusNotifierWatcher class implements the StatusNotifierWatcher dbus object
|
||||
*/
|
||||
export class StatusNotifierWatcher {
|
||||
constructor(watchDog) {
|
||||
this._watchDog = watchDog;
|
||||
this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(Interfaces.StatusNotifierWatcher, this);
|
||||
try {
|
||||
this._dbusImpl.export(Gio.DBus.session, WATCHER_OBJECT);
|
||||
} catch (e) {
|
||||
Util.Logger.warn(`Failed to export ${WATCHER_OBJECT}`);
|
||||
logError(e);
|
||||
}
|
||||
this._cancellable = new Gio.Cancellable();
|
||||
this._everAcquiredName = false;
|
||||
this._ownName = Gio.DBus.session.own_name(WATCHER_BUS_NAME,
|
||||
Gio.BusNameOwnerFlags.NONE,
|
||||
this._acquiredName.bind(this),
|
||||
this._lostName.bind(this));
|
||||
this._items = new Map();
|
||||
|
||||
try {
|
||||
this._dbusImpl.emit_signal('StatusNotifierHostRegistered', null);
|
||||
} catch (e) {
|
||||
Util.Logger.warn(`Failed to notify registered host ${WATCHER_OBJECT}`);
|
||||
}
|
||||
|
||||
this._seekStatusNotifierItems().catch(e => {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e, 'Looking for StatusNotifierItem\'s');
|
||||
});
|
||||
}
|
||||
|
||||
_acquiredName() {
|
||||
this._everAcquiredName = true;
|
||||
this._watchDog.nameAcquired = true;
|
||||
}
|
||||
|
||||
_lostName() {
|
||||
if (this._everAcquiredName)
|
||||
Util.Logger.debug(`Lost name${WATCHER_BUS_NAME}`);
|
||||
else
|
||||
Util.Logger.warn(`Failed to acquire ${WATCHER_BUS_NAME}`);
|
||||
this._watchDog.nameAcquired = false;
|
||||
}
|
||||
|
||||
async _registerItem(service, busName, objPath) {
|
||||
const id = Util.indicatorId(service, busName, objPath);
|
||||
|
||||
if (this._items.has(id)) {
|
||||
Util.Logger.warn(`Item ${id} is already registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
Util.Logger.debug(`Registering StatusNotifierItem ${id}`);
|
||||
|
||||
try {
|
||||
const indicator = new AppIndicator.AppIndicator(service, busName, objPath);
|
||||
this._items.set(id, indicator);
|
||||
indicator.connect('destroy', () => this._onIndicatorDestroyed(indicator));
|
||||
|
||||
indicator.connect('name-owner-changed', async () => {
|
||||
if (!indicator.hasNameOwner) {
|
||||
try {
|
||||
await new PromiseUtils.TimeoutPromise(500,
|
||||
GLib.PRIORITY_DEFAULT, this._cancellable);
|
||||
if (this._items.has(id) && !indicator.hasNameOwner)
|
||||
indicator.destroy();
|
||||
} catch (e) {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// if the desktop is not ready delay the icon creation and signal emissions
|
||||
await Util.waitForStartupCompletion(indicator.cancellable);
|
||||
const statusIcon = new IndicatorStatusIcon.IndicatorStatusIcon(indicator);
|
||||
IndicatorStatusIcon.addIconToPanel(statusIcon);
|
||||
|
||||
this._dbusImpl.emit_signal('StatusNotifierItemRegistered',
|
||||
GLib.Variant.new('(s)', [indicator.uniqueId]));
|
||||
this._dbusImpl.emit_property_changed('RegisteredStatusNotifierItems',
|
||||
GLib.Variant.new('as', this.RegisteredStatusNotifierItems));
|
||||
} catch (e) {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async _ensureItemRegistered(service, busName, objPath) {
|
||||
const id = Util.indicatorId(service, busName, objPath);
|
||||
const item = this._items.get(id);
|
||||
|
||||
if (item) {
|
||||
// delete the old one and add the new indicator
|
||||
Util.Logger.debug(`Attempting to re-register ${id}; resetting instead`);
|
||||
item.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
await this._registerItem(service, busName, objPath);
|
||||
}
|
||||
|
||||
async _seekStatusNotifierItems() {
|
||||
// Some indicators (*coff*, dropbox, *coff*) do not re-register again
|
||||
// when the plugin is enabled/disabled, thus we need to manually look
|
||||
// for the objects in the session bus that implements the
|
||||
// StatusNotifierItem interface... However let's do it after a low
|
||||
// priority idle, so that it won't affect startup.
|
||||
const cancellable = this._cancellable;
|
||||
const bus = Gio.DBus.session;
|
||||
const uniqueNames = await Util.getBusNames(bus, cancellable);
|
||||
const introspectName = async name => {
|
||||
const nodes = Util.introspectBusObject(bus, name, cancellable,
|
||||
['org.kde.StatusNotifierItem']);
|
||||
const services = [...uniqueNames.get(name)];
|
||||
|
||||
for await (const node of nodes) {
|
||||
const {path} = node;
|
||||
const ids = services.map(s => Util.indicatorId(s, name, path));
|
||||
if (ids.every(id => !this._items.has(id))) {
|
||||
const service = services.find(s =>
|
||||
s && s.startsWith('org.kde.StatusNotifierItem')) || services[0];
|
||||
const id = Util.indicatorId(
|
||||
path === DEFAULT_ITEM_OBJECT_PATH ? service : null,
|
||||
name, path);
|
||||
Util.Logger.warn(`Using Brute-force mode for StatusNotifierItem ${id}`);
|
||||
this._registerItem(service, name, path);
|
||||
}
|
||||
}
|
||||
};
|
||||
await Promise.allSettled([...uniqueNames.keys()].map(n => introspectName(n)));
|
||||
}
|
||||
|
||||
async RegisterStatusNotifierItemAsync(params, invocation) {
|
||||
// it would be too easy if all application behaved the same
|
||||
// instead, ayatana patched gnome apps to send a path
|
||||
// while kde apps send a bus name
|
||||
const [service] = params;
|
||||
let busName, objPath;
|
||||
|
||||
if (service.charAt(0) === '/') { // looks like a path
|
||||
busName = invocation.get_sender();
|
||||
objPath = service;
|
||||
} else if (service.match(Util.BUS_ADDRESS_REGEX)) {
|
||||
try {
|
||||
busName = await Util.getUniqueBusName(invocation.get_connection(),
|
||||
service, this._cancellable);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
objPath = DEFAULT_ITEM_OBJECT_PATH;
|
||||
}
|
||||
|
||||
if (!busName || !objPath) {
|
||||
const error = `Impossible to register an indicator for parameters '${
|
||||
service.toString()}'`;
|
||||
Util.Logger.warn(error);
|
||||
|
||||
invocation.return_dbus_error('org.gnome.gjs.JSError.ValueError',
|
||||
error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this._ensureItemRegistered(service, busName, objPath);
|
||||
invocation.return_value(null);
|
||||
} catch (e) {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e);
|
||||
invocation.return_dbus_error('org.gnome.gjs.JSError.ValueError',
|
||||
e.message);
|
||||
}
|
||||
}
|
||||
|
||||
_onIndicatorDestroyed(indicator) {
|
||||
const {uniqueId} = indicator;
|
||||
this._items.delete(uniqueId);
|
||||
|
||||
try {
|
||||
this._dbusImpl.emit_signal('StatusNotifierItemUnregistered',
|
||||
GLib.Variant.new('(s)', [uniqueId]));
|
||||
this._dbusImpl.emit_property_changed('RegisteredStatusNotifierItems',
|
||||
GLib.Variant.new('as', this.RegisteredStatusNotifierItems));
|
||||
} catch (e) {
|
||||
Util.Logger.warn(`Failed to emit signals: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
RegisterStatusNotifierHostAsync(_service, invocation) {
|
||||
invocation.return_error_literal(
|
||||
Gio.DBusError,
|
||||
Gio.DBusError.NOT_SUPPORTED,
|
||||
'Registering additional notification hosts is not supported');
|
||||
}
|
||||
|
||||
IsNotificationHostRegistered() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get RegisteredStatusNotifierItems() {
|
||||
return Array.from(this._items.values()).map(i => i.uniqueId);
|
||||
}
|
||||
|
||||
get IsStatusNotifierHostRegistered() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get ProtocolVersion() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this._isDestroyed)
|
||||
return;
|
||||
|
||||
// this doesn't do any sync operation and doesn't allow us to hook up
|
||||
// the event of being finished which results in our unholy debounce hack
|
||||
// (see extension.js)
|
||||
this._items.forEach(indicator => indicator.destroy());
|
||||
this._cancellable.cancel();
|
||||
|
||||
try {
|
||||
this._dbusImpl.emit_signal('StatusNotifierHostUnregistered', null);
|
||||
} catch (e) {
|
||||
Util.Logger.warn(`Failed to emit uinregistered signal: ${e}`);
|
||||
}
|
||||
|
||||
Gio.DBus.session.unown_name(this._ownName);
|
||||
|
||||
try {
|
||||
this._dbusImpl.unexport();
|
||||
} catch (e) {
|
||||
Util.Logger.warn(`Failed to unexport watcher object: ${e}`);
|
||||
}
|
||||
|
||||
DBusMenu.DBusClient.destroy();
|
||||
AppIndicator.AppIndicatorProxy.destroy();
|
||||
DBusProxy.destroy();
|
||||
Util.destroyDefaultTheme();
|
||||
|
||||
this._dbusImpl.run_dispose();
|
||||
delete this._dbusImpl;
|
||||
|
||||
delete this._items;
|
||||
this._isDestroyed = true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import Shell from 'gi://Shell';
|
||||
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
import * as Signals from 'resource:///org/gnome/shell/misc/signals.js';
|
||||
|
||||
import * as IndicatorStatusIcon from './indicatorStatusIcon.js';
|
||||
import * as Util from './util.js';
|
||||
import * as SettingsManager from './settingsManager.js';
|
||||
|
||||
let trayIconsManager;
|
||||
|
||||
export class TrayIconsManager extends Signals.EventEmitter {
|
||||
static initialize() {
|
||||
if (!trayIconsManager)
|
||||
trayIconsManager = new TrayIconsManager();
|
||||
return trayIconsManager;
|
||||
}
|
||||
|
||||
static destroy() {
|
||||
trayIconsManager.destroy();
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
if (trayIconsManager)
|
||||
throw new Error('TrayIconsManager is already constructed');
|
||||
|
||||
this._changedId = SettingsManager.getDefaultGSettings().connect(
|
||||
'changed::legacy-tray-enabled', () => this._toggle());
|
||||
|
||||
this._toggle();
|
||||
}
|
||||
|
||||
_toggle() {
|
||||
if (SettingsManager.getDefaultGSettings().get_boolean('legacy-tray-enabled'))
|
||||
this._enable();
|
||||
else
|
||||
this._disable();
|
||||
}
|
||||
|
||||
_enable() {
|
||||
if (this._tray)
|
||||
return;
|
||||
|
||||
this._tray = new Shell.TrayManager();
|
||||
Util.connectSmart(this._tray, 'tray-icon-added', this, this.onTrayIconAdded);
|
||||
Util.connectSmart(this._tray, 'tray-icon-removed', this, this.onTrayIconRemoved);
|
||||
|
||||
this._tray.manage_screen(Main.panel);
|
||||
}
|
||||
|
||||
_disable() {
|
||||
if (!this._tray)
|
||||
return;
|
||||
|
||||
IndicatorStatusIcon.getTrayIcons().forEach(i => i.destroy());
|
||||
if (this._tray.unmanage_screen) {
|
||||
this._tray.unmanage_screen();
|
||||
this._tray = null;
|
||||
} else {
|
||||
// FIXME: This is very ugly, but it's needed by old shell versions
|
||||
this._tray = null;
|
||||
imports.system.gc(); // force finalizing tray to unmanage screen
|
||||
}
|
||||
}
|
||||
|
||||
onTrayIconAdded(_tray, icon) {
|
||||
const trayIcon = new IndicatorStatusIcon.IndicatorStatusTrayIcon(icon);
|
||||
IndicatorStatusIcon.addIconToPanel(trayIcon);
|
||||
}
|
||||
|
||||
onTrayIconRemoved(_tray, icon) {
|
||||
try {
|
||||
const [trayIcon] = IndicatorStatusIcon.getTrayIcons().filter(i => i.icon === icon);
|
||||
trayIcon.destroy();
|
||||
} catch (e) {
|
||||
Util.Logger.warning(`No icon container found for ${icon.title} (${icon})`);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.emit('destroy');
|
||||
SettingsManager.getDefaultGSettings().disconnect(this._changedId);
|
||||
this._disable();
|
||||
trayIconsManager = null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,434 @@
|
|||
// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
import St from 'gi://St';
|
||||
|
||||
const ByteArray = imports.byteArray;
|
||||
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
import * as Config from 'resource:///org/gnome/shell/misc/config.js';
|
||||
import * as Signals from 'resource:///org/gnome/shell/misc/signals.js';
|
||||
|
||||
import {BaseStatusIcon} from './indicatorStatusIcon.js';
|
||||
|
||||
export const BUS_ADDRESS_REGEX = /([a-zA-Z0-9._-]+\.[a-zA-Z0-9.-]+)|(:[0-9]+\.[0-9]+)$/;
|
||||
|
||||
Gio._promisify(Gio.DBusConnection.prototype, 'call');
|
||||
Gio._promisify(Gio._LocalFilePrototype, 'read');
|
||||
Gio._promisify(Gio.InputStream.prototype, 'read_bytes_async');
|
||||
|
||||
export function indicatorId(service, busName, objectPath) {
|
||||
if (service !== busName && service?.match(BUS_ADDRESS_REGEX))
|
||||
return service;
|
||||
|
||||
return `${busName}@${objectPath}`;
|
||||
}
|
||||
|
||||
export async function getUniqueBusName(bus, name, cancellable) {
|
||||
if (name[0] === ':')
|
||||
return name;
|
||||
|
||||
if (!bus)
|
||||
bus = Gio.DBus.session;
|
||||
|
||||
const variantName = new GLib.Variant('(s)', [name]);
|
||||
const [unique] = (await bus.call('org.freedesktop.DBus', '/', 'org.freedesktop.DBus',
|
||||
'GetNameOwner', variantName, new GLib.VariantType('(s)'),
|
||||
Gio.DBusCallFlags.NONE, -1, cancellable)).deep_unpack();
|
||||
|
||||
return unique;
|
||||
}
|
||||
|
||||
export async function getBusNames(bus, cancellable) {
|
||||
if (!bus)
|
||||
bus = Gio.DBus.session;
|
||||
|
||||
const [names] = (await bus.call('org.freedesktop.DBus', '/', 'org.freedesktop.DBus',
|
||||
'ListNames', null, new GLib.VariantType('(as)'), Gio.DBusCallFlags.NONE,
|
||||
-1, cancellable)).deep_unpack();
|
||||
|
||||
const uniqueNames = new Map();
|
||||
const requests = names.map(name => getUniqueBusName(bus, name, cancellable));
|
||||
const results = await Promise.allSettled(requests);
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const result = results[i];
|
||||
if (result.status === 'fulfilled') {
|
||||
let namesForBus = uniqueNames.get(result.value);
|
||||
if (!namesForBus) {
|
||||
namesForBus = new Set();
|
||||
uniqueNames.set(result.value, namesForBus);
|
||||
}
|
||||
namesForBus.add(result.value !== names[i] ? names[i] : null);
|
||||
} else if (!result.reason.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
|
||||
Logger.debug(`Impossible to get the unique name of ${names[i]}: ${result.reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueNames;
|
||||
}
|
||||
|
||||
async function getProcessId(connectionName, cancellable = null, bus = Gio.DBus.session) {
|
||||
const res = await bus.call('org.freedesktop.DBus', '/',
|
||||
'org.freedesktop.DBus', 'GetConnectionUnixProcessID',
|
||||
new GLib.Variant('(s)', [connectionName]),
|
||||
new GLib.VariantType('(u)'),
|
||||
Gio.DBusCallFlags.NONE,
|
||||
-1,
|
||||
cancellable);
|
||||
const [pid] = res.deepUnpack();
|
||||
return pid;
|
||||
}
|
||||
|
||||
export async function getProcessName(connectionName, cancellable = null,
|
||||
priority = GLib.PRIORITY_DEFAULT, bus = Gio.DBus.session) {
|
||||
const pid = await getProcessId(connectionName, cancellable, bus);
|
||||
const cmdFile = Gio.File.new_for_path(`/proc/${pid}/cmdline`);
|
||||
const inputStream = await cmdFile.read_async(priority, cancellable);
|
||||
const bytes = await inputStream.read_bytes_async(2048, priority, cancellable);
|
||||
return ByteArray.toString(bytes.toArray().map(v => !v ? 0x20 : v));
|
||||
}
|
||||
|
||||
export async function* introspectBusObject(bus, name, cancellable,
|
||||
interfaces = undefined, path = undefined) {
|
||||
if (!path)
|
||||
path = '/';
|
||||
|
||||
const [introspection] = (await bus.call(name, path, 'org.freedesktop.DBus.Introspectable',
|
||||
'Introspect', null, new GLib.VariantType('(s)'), Gio.DBusCallFlags.NONE,
|
||||
5000, cancellable)).deep_unpack();
|
||||
|
||||
const nodeInfo = Gio.DBusNodeInfo.new_for_xml(introspection);
|
||||
|
||||
if (!interfaces || dbusNodeImplementsInterfaces(nodeInfo, interfaces))
|
||||
yield {nodeInfo, path};
|
||||
|
||||
if (path === '/')
|
||||
path = '';
|
||||
|
||||
for (const subNodeInfo of nodeInfo.nodes) {
|
||||
const subPath = `${path}/${subNodeInfo.path}`;
|
||||
yield* introspectBusObject(bus, name, cancellable, interfaces, subPath);
|
||||
}
|
||||
}
|
||||
|
||||
function dbusNodeImplementsInterfaces(nodeInfo, interfaces) {
|
||||
if (!(nodeInfo instanceof Gio.DBusNodeInfo) || !Array.isArray(interfaces))
|
||||
return false;
|
||||
|
||||
return interfaces.some(iface => nodeInfo.lookup_interface(iface));
|
||||
}
|
||||
|
||||
export class NameWatcher extends Signals.EventEmitter {
|
||||
constructor(name) {
|
||||
super();
|
||||
|
||||
this._watcherId = Gio.DBus.session.watch_name(name,
|
||||
Gio.BusNameWatcherFlags.NONE, () => {
|
||||
this._nameOnBus = true;
|
||||
Logger.debug(`Name ${name} appeared`);
|
||||
this.emit('changed');
|
||||
this.emit('appeared');
|
||||
}, () => {
|
||||
this._nameOnBus = false;
|
||||
Logger.debug(`Name ${name} vanished`);
|
||||
this.emit('changed');
|
||||
this.emit('vanished');
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.emit('destroy');
|
||||
|
||||
Gio.DBus.session.unwatch_name(this._watcherId);
|
||||
delete this._watcherId;
|
||||
}
|
||||
|
||||
get nameOnBus() {
|
||||
return !!this._nameOnBus;
|
||||
}
|
||||
}
|
||||
|
||||
function connectSmart3A(src, signal, handler) {
|
||||
const id = src.connect(signal, handler);
|
||||
let destroyId = 0;
|
||||
|
||||
if (src.connect && (!(src instanceof GObject.Object) || GObject.signal_lookup('destroy', src))) {
|
||||
destroyId = src.connect('destroy', () => {
|
||||
src.disconnect(id);
|
||||
src.disconnect(destroyId);
|
||||
});
|
||||
}
|
||||
|
||||
return [id, destroyId];
|
||||
}
|
||||
|
||||
function connectSmart4A(src, signal, target, method) {
|
||||
if (typeof method !== 'function')
|
||||
throw new TypeError('Unsupported function');
|
||||
|
||||
method = method.bind(target);
|
||||
const signalId = src.connect(signal, method);
|
||||
const onDestroy = () => {
|
||||
src.disconnect(signalId);
|
||||
if (srcDestroyId)
|
||||
src.disconnect(srcDestroyId);
|
||||
if (tgtDestroyId)
|
||||
target.disconnect(tgtDestroyId);
|
||||
};
|
||||
|
||||
// GObject classes might or might not have a destroy signal
|
||||
// JS Classes will not complain when connecting to non-existent signals
|
||||
const srcDestroyId = src.connect && (!(src instanceof GObject.Object) ||
|
||||
GObject.signal_lookup('destroy', src)) ? src.connect('destroy', onDestroy) : 0;
|
||||
const tgtDestroyId = target.connect && (!(target instanceof GObject.Object) ||
|
||||
GObject.signal_lookup('destroy', target)) ? target.connect('destroy', onDestroy) : 0;
|
||||
|
||||
return [signalId, srcDestroyId, tgtDestroyId];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line valid-jsdoc
|
||||
/**
|
||||
* Connect signals to slots, and remove the connection when either source or
|
||||
* target are destroyed
|
||||
*
|
||||
* Usage:
|
||||
* Util.connectSmart(srcOb, 'signal', tgtObj, 'handler')
|
||||
* or
|
||||
* Util.connectSmart(srcOb, 'signal', () => { ... })
|
||||
*/
|
||||
export function connectSmart(...args) {
|
||||
if (arguments.length === 4)
|
||||
return connectSmart4A(...args);
|
||||
else
|
||||
return connectSmart3A(...args);
|
||||
}
|
||||
|
||||
function disconnectSmart3A(src, signalIds) {
|
||||
const [id, destroyId] = signalIds;
|
||||
src.disconnect(id);
|
||||
|
||||
if (destroyId)
|
||||
src.disconnect(destroyId);
|
||||
}
|
||||
|
||||
function disconnectSmart4A(src, tgt, signalIds) {
|
||||
const [signalId, srcDestroyId, tgtDestroyId] = signalIds;
|
||||
|
||||
disconnectSmart3A(src, [signalId, srcDestroyId]);
|
||||
|
||||
if (tgtDestroyId)
|
||||
tgt.disconnect(tgtDestroyId);
|
||||
}
|
||||
|
||||
export function disconnectSmart(...args) {
|
||||
if (arguments.length === 2)
|
||||
return disconnectSmart3A(...args);
|
||||
else if (arguments.length === 3)
|
||||
return disconnectSmart4A(...args);
|
||||
|
||||
throw new TypeError('Unexpected number of arguments');
|
||||
}
|
||||
|
||||
let _defaultTheme;
|
||||
export function getDefaultTheme() {
|
||||
if (_defaultTheme)
|
||||
return _defaultTheme;
|
||||
|
||||
_defaultTheme = new St.IconTheme();
|
||||
return _defaultTheme;
|
||||
}
|
||||
|
||||
export function destroyDefaultTheme() {
|
||||
_defaultTheme = null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line valid-jsdoc
|
||||
/**
|
||||
* Helper function to wait for the system startup to be completed.
|
||||
* Adding widgets before the desktop is ready to accept them can result in errors.
|
||||
*/
|
||||
export async function waitForStartupCompletion(cancellable) {
|
||||
if (Main.layoutManager._startingUp)
|
||||
await Main.layoutManager.connect_once('startup-complete', cancellable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for logging stuff
|
||||
*/
|
||||
export class Logger {
|
||||
static _logStructured(logLevel, message, extraFields = {}) {
|
||||
if (!Object.values(GLib.LogLevelFlags).includes(logLevel)) {
|
||||
Logger._logStructured(GLib.LogLevelFlags.LEVEL_WARNING,
|
||||
'logLevel is not a valid GLib.LogLevelFlags');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Logger._levels.includes(logLevel))
|
||||
return;
|
||||
|
||||
let fields = {
|
||||
'SYSLOG_IDENTIFIER': this.uuid,
|
||||
'MESSAGE': `${message}`,
|
||||
};
|
||||
|
||||
let thisFile = null;
|
||||
const {stack} = new Error();
|
||||
for (let stackLine of stack.split('\n')) {
|
||||
stackLine = stackLine.replace('resource:///org/gnome/Shell/', '');
|
||||
const [code, line] = stackLine.split(':');
|
||||
const [func, file] = code.split(/@(.+)/);
|
||||
|
||||
if (!thisFile || thisFile === file) {
|
||||
thisFile = file;
|
||||
continue;
|
||||
}
|
||||
|
||||
fields = Object.assign(fields, {
|
||||
'CODE_FILE': file || '',
|
||||
'CODE_LINE': line || '',
|
||||
'CODE_FUNC': func || '',
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
GLib.log_structured(Logger._domain, logLevel, Object.assign(fields, extraFields));
|
||||
}
|
||||
|
||||
static init(extension) {
|
||||
if (Logger._domain)
|
||||
return;
|
||||
|
||||
const allLevels = Object.values(GLib.LogLevelFlags);
|
||||
const domains = GLib.getenv('G_MESSAGES_DEBUG');
|
||||
const {name: domain} = extension.metadata;
|
||||
this.uuid = extension.metadata.uuid;
|
||||
Logger._domain = domain.replaceAll(' ', '-');
|
||||
|
||||
if (domains === 'all' || (domains && domains.split(' ').includes(Logger._domain))) {
|
||||
Logger._levels = allLevels;
|
||||
} else {
|
||||
Logger._levels = allLevels.filter(
|
||||
l => l <= GLib.LogLevelFlags.LEVEL_WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
static debug(message) {
|
||||
Logger._logStructured(GLib.LogLevelFlags.LEVEL_DEBUG, message);
|
||||
}
|
||||
|
||||
static message(message) {
|
||||
Logger._logStructured(GLib.LogLevelFlags.LEVEL_MESSAGE, message);
|
||||
}
|
||||
|
||||
static warn(message) {
|
||||
Logger._logStructured(GLib.LogLevelFlags.LEVEL_WARNING, message);
|
||||
}
|
||||
|
||||
static error(message) {
|
||||
Logger._logStructured(GLib.LogLevelFlags.LEVEL_ERROR, message);
|
||||
}
|
||||
|
||||
static critical(message) {
|
||||
Logger._logStructured(GLib.LogLevelFlags.LEVEL_CRITICAL, message);
|
||||
}
|
||||
}
|
||||
|
||||
export function versionCheck(required) {
|
||||
const current = Config.PACKAGE_VERSION;
|
||||
const currentArray = current.split('.');
|
||||
const [major] = currentArray;
|
||||
return major >= required;
|
||||
}
|
||||
|
||||
export function tryCleanupOldIndicators() {
|
||||
const indicatorType = BaseStatusIcon;
|
||||
const indicators = Object.values(Main.panel.statusArea).filter(i => i instanceof indicatorType);
|
||||
|
||||
try {
|
||||
const panelBoxes = [
|
||||
Main.panel._leftBox, Main.panel._centerBox, Main.panel._rightBox,
|
||||
];
|
||||
|
||||
panelBoxes.forEach(box =>
|
||||
indicators.push(...box.get_children().filter(i => i instanceof indicatorType)));
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
|
||||
new Set(indicators).forEach(i => i.destroy());
|
||||
}
|
||||
|
||||
export const CancellableChild = GObject.registerClass({
|
||||
Properties: {
|
||||
'parent': GObject.ParamSpec.object(
|
||||
'parent', 'parent', 'parent',
|
||||
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
|
||||
Gio.Cancellable.$gtype),
|
||||
},
|
||||
},
|
||||
class CancellableChild extends Gio.Cancellable {
|
||||
_init(parent) {
|
||||
if (parent && !(parent instanceof Gio.Cancellable))
|
||||
throw TypeError('Not a valid cancellable');
|
||||
|
||||
super._init({parent});
|
||||
|
||||
if (parent) {
|
||||
if (parent.is_cancelled()) {
|
||||
this.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
this._connectToParent();
|
||||
}
|
||||
}
|
||||
|
||||
_connectToParent() {
|
||||
this._connectId = this.parent.connect(() => {
|
||||
this._realCancel();
|
||||
|
||||
if (this._disconnectIdle)
|
||||
return;
|
||||
|
||||
this._disconnectIdle = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
|
||||
delete this._disconnectIdle;
|
||||
this._disconnectFromParent();
|
||||
return GLib.SOURCE_REMOVE;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_disconnectFromParent() {
|
||||
if (this._connectId && !this._disconnectIdle) {
|
||||
this.parent.disconnect(this._connectId);
|
||||
delete this._connectId;
|
||||
}
|
||||
}
|
||||
|
||||
_realCancel() {
|
||||
Gio.Cancellable.prototype.cancel.call(this);
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this._disconnectFromParent();
|
||||
this._realCancel();
|
||||
}
|
||||
});
|
|
@ -0,0 +1,262 @@
|
|||
import Shell from 'gi://Shell';
|
||||
import Clutter from 'gi://Clutter';
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
|
||||
import { PaintSignals } from '../effects/paint_signals.js';
|
||||
const Tweener = imports.tweener.tweener;
|
||||
|
||||
const transparent = Clutter.Color.from_pixel(0x00000000);
|
||||
const FOLDER_DIALOG_ANIMATION_TIME = 200;
|
||||
|
||||
const DIALOGS_STYLES = [
|
||||
"",
|
||||
"appfolder-dialogs-transparent",
|
||||
"appfolder-dialogs-light",
|
||||
"appfolder-dialogs-dark"
|
||||
];
|
||||
|
||||
let original_zoomAndFadeIn = null;
|
||||
let original_zoomAndFadeOut = null;
|
||||
let sigma;
|
||||
let brightness;
|
||||
|
||||
let _zoomAndFadeIn = function () {
|
||||
let [sourceX, sourceY] =
|
||||
this._source.get_transformed_position();
|
||||
let [dialogX, dialogY] =
|
||||
this.child.get_transformed_position();
|
||||
|
||||
this.child.set({
|
||||
translation_x: sourceX - dialogX,
|
||||
translation_y: sourceY - dialogY,
|
||||
scale_x: this._source.width / this.child.width,
|
||||
scale_y: this._source.height / this.child.height,
|
||||
opacity: 0,
|
||||
});
|
||||
|
||||
this.set_background_color(transparent);
|
||||
|
||||
let blur_effect = this.get_effect("appfolder-blur");
|
||||
|
||||
blur_effect.sigma = 0;
|
||||
blur_effect.brightness = 1.0;
|
||||
Tweener.addTween(blur_effect,
|
||||
{
|
||||
sigma: sigma,
|
||||
brightness: brightness,
|
||||
time: FOLDER_DIALOG_ANIMATION_TIME / 1000,
|
||||
transition: 'easeOutQuad'
|
||||
}
|
||||
);
|
||||
|
||||
this.child.ease({
|
||||
translation_x: 0,
|
||||
translation_y: 0,
|
||||
scale_x: 1,
|
||||
scale_y: 1,
|
||||
opacity: 255,
|
||||
duration: FOLDER_DIALOG_ANIMATION_TIME,
|
||||
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
||||
});
|
||||
|
||||
this._needsZoomAndFade = false;
|
||||
|
||||
if (this._sourceMappedId === 0) {
|
||||
this._sourceMappedId = this._source.connect(
|
||||
'notify::mapped', this._zoomAndFadeOut.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
let _zoomAndFadeOut = function () {
|
||||
if (!this._isOpen)
|
||||
return;
|
||||
|
||||
if (!this._source.mapped) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
let [sourceX, sourceY] =
|
||||
this._source.get_transformed_position();
|
||||
let [dialogX, dialogY] =
|
||||
this.child.get_transformed_position();
|
||||
|
||||
this.set_background_color(transparent);
|
||||
|
||||
let blur_effect = this.get_effect("appfolder-blur");
|
||||
Tweener.addTween(blur_effect,
|
||||
{
|
||||
sigma: 0,
|
||||
brightness: 1.0,
|
||||
time: FOLDER_DIALOG_ANIMATION_TIME / 1000,
|
||||
transition: 'easeInQuad'
|
||||
}
|
||||
);
|
||||
|
||||
this.child.ease({
|
||||
translation_x: sourceX - dialogX,
|
||||
translation_y: sourceY - dialogY,
|
||||
scale_x: this._source.width / this.child.width,
|
||||
scale_y: this._source.height / this.child.height,
|
||||
opacity: 0,
|
||||
duration: FOLDER_DIALOG_ANIMATION_TIME,
|
||||
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
||||
onComplete: () => {
|
||||
this.child.set({
|
||||
translation_x: 0,
|
||||
translation_y: 0,
|
||||
scale_x: 1,
|
||||
scale_y: 1,
|
||||
opacity: 255,
|
||||
});
|
||||
this.hide();
|
||||
|
||||
this._popdownCallbacks.forEach(func => func());
|
||||
this._popdownCallbacks = [];
|
||||
},
|
||||
});
|
||||
|
||||
this._needsZoomAndFade = false;
|
||||
};
|
||||
|
||||
|
||||
export const AppFoldersBlur = class AppFoldersBlur {
|
||||
constructor(connections, settings, _) {
|
||||
this.connections = connections;
|
||||
this.paint_signals = new PaintSignals(connections);
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
enable() {
|
||||
this._log("blurring appfolders");
|
||||
|
||||
brightness = this.settings.appfolder.CUSTOMIZE
|
||||
? this.settings.appfolder.BRIGHTNESS
|
||||
: this.settings.BRIGHTNESS;
|
||||
sigma = this.settings.appfolder.CUSTOMIZE
|
||||
? this.settings.appfolder.SIGMA
|
||||
: this.settings.SIGMA;
|
||||
|
||||
let appDisplay = Main.overview._overview.controls._appDisplay;
|
||||
|
||||
if (appDisplay._folderIcons.length > 0) {
|
||||
this.blur_appfolders();
|
||||
}
|
||||
|
||||
this.connections.connect(
|
||||
appDisplay, 'view-loaded', this.blur_appfolders.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
blur_appfolders() {
|
||||
let appDisplay = Main.overview._overview.controls._appDisplay;
|
||||
|
||||
if (this.settings.HACKS_LEVEL === 1 || this.settings.HACKS_LEVEL === 2)
|
||||
this._log(`appfolders hack level ${this.settings.HACKS_LEVEL}`);
|
||||
|
||||
appDisplay._folderIcons.forEach(icon => {
|
||||
icon._ensureFolderDialog();
|
||||
|
||||
if (original_zoomAndFadeIn == null) {
|
||||
original_zoomAndFadeIn = icon._dialog._zoomAndFadeIn;
|
||||
}
|
||||
if (original_zoomAndFadeOut == null) {
|
||||
original_zoomAndFadeOut = icon._dialog._zoomAndFadeOut;
|
||||
}
|
||||
|
||||
let blur_effect = new Shell.BlurEffect({
|
||||
name: "appfolder-blur",
|
||||
sigma: sigma,
|
||||
brightness: brightness,
|
||||
mode: Shell.BlurMode.BACKGROUND
|
||||
});
|
||||
|
||||
icon._dialog.remove_effect_by_name("appfolder-blur");
|
||||
icon._dialog.add_effect(blur_effect);
|
||||
|
||||
DIALOGS_STYLES.forEach(
|
||||
style => icon._dialog._viewBox.remove_style_class_name(style)
|
||||
);
|
||||
|
||||
icon._dialog._viewBox.add_style_class_name(
|
||||
DIALOGS_STYLES[this.settings.appfolder.STYLE_DIALOGS]
|
||||
);
|
||||
|
||||
// finally override the builtin functions
|
||||
|
||||
icon._dialog._zoomAndFadeIn = _zoomAndFadeIn;
|
||||
icon._dialog._zoomAndFadeOut = _zoomAndFadeOut;
|
||||
|
||||
|
||||
// HACK
|
||||
//
|
||||
//`Shell.BlurEffect` does not repaint when shadows are under it. [1]
|
||||
//
|
||||
// This does not entirely fix this bug (shadows caused by windows
|
||||
// still cause artifacts), but it prevents the shadows of the panel
|
||||
// buttons to cause artifacts on the panel itself
|
||||
//
|
||||
// [1]: https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/2857
|
||||
|
||||
if (this.settings.HACKS_LEVEL === 1 || this.settings.HACKS_LEVEL === 2) {
|
||||
this.paint_signals.disconnect_all_for_actor(icon._dialog);
|
||||
this.paint_signals.connect(icon._dialog, blur_effect);
|
||||
} else {
|
||||
this.paint_signals.disconnect_all();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
set_sigma(s) {
|
||||
sigma = s;
|
||||
if (this.settings.appfolder.BLUR)
|
||||
this.blur_appfolders();
|
||||
}
|
||||
|
||||
set_brightness(b) {
|
||||
brightness = b;
|
||||
if (this.settings.appfolder.BLUR)
|
||||
this.blur_appfolders();
|
||||
}
|
||||
|
||||
// not implemented for dynamic blur
|
||||
set_color(c) { }
|
||||
set_noise_amount(n) { }
|
||||
set_noise_lightness(l) { }
|
||||
|
||||
disable() {
|
||||
this._log("removing blur from appfolders");
|
||||
|
||||
let appDisplay = Main.overview._overview.controls._appDisplay;
|
||||
|
||||
if (original_zoomAndFadeIn != null) {
|
||||
appDisplay._folderIcons.forEach(icon => {
|
||||
if (icon._dialog)
|
||||
icon._dialog._zoomAndFadeIn = original_zoomAndFadeIn;
|
||||
});
|
||||
}
|
||||
|
||||
if (original_zoomAndFadeOut != null) {
|
||||
appDisplay._folderIcons.forEach(icon => {
|
||||
if (icon._dialog)
|
||||
icon._dialog._zoomAndFadeOut = original_zoomAndFadeOut;
|
||||
});
|
||||
}
|
||||
|
||||
appDisplay._folderIcons.forEach(icon => {
|
||||
if (icon._dialog) {
|
||||
icon._dialog.remove_effect_by_name("appfolder-blur");
|
||||
DIALOGS_STYLES.forEach(
|
||||
s => icon._dialog._viewBox.remove_style_class_name(s)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.connections.disconnect_all();
|
||||
}
|
||||
|
||||
_log(str) {
|
||||
if (this.settings.DEBUG)
|
||||
console.log(`[Blur my Shell > appfolders] ${str}`);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,548 @@
|
|||
import Shell from 'gi://Shell';
|
||||
import Clutter from 'gi://Clutter';
|
||||
import Meta from 'gi://Meta';
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
|
||||
import { PaintSignals } from '../effects/paint_signals.js';
|
||||
import { ApplicationsService } from '../dbus/services.js';
|
||||
|
||||
|
||||
export const ApplicationsBlur = class ApplicationsBlur {
|
||||
constructor(connections, settings, _) {
|
||||
this.connections = connections;
|
||||
this.settings = settings;
|
||||
this.paint_signals = new PaintSignals(connections);
|
||||
|
||||
// stores every blurred window
|
||||
this.window_map = new Map();
|
||||
// stores every blur actor
|
||||
this.blur_actor_map = new Map();
|
||||
}
|
||||
|
||||
enable() {
|
||||
this._log("blurring applications...");
|
||||
|
||||
// export dbus service for preferences
|
||||
this.service = new ApplicationsService;
|
||||
this.service.export();
|
||||
|
||||
// blur already existing windows
|
||||
this.update_all_windows();
|
||||
|
||||
// blur every new window
|
||||
this.connections.connect(
|
||||
global.display,
|
||||
'window-created',
|
||||
(_meta_display, meta_window) => {
|
||||
this._log("window created");
|
||||
|
||||
if (meta_window) {
|
||||
let window_actor = meta_window.get_compositor_private();
|
||||
this.track_new(window_actor, meta_window);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.connect_to_overview();
|
||||
}
|
||||
|
||||
/// Connect to the overview being opened/closed to force the blur being
|
||||
/// shown on every window of the workspaces viewer.
|
||||
connect_to_overview() {
|
||||
this.connections.disconnect_all_for(Main.overview);
|
||||
|
||||
if (this.settings.applications.BLUR_ON_OVERVIEW) {
|
||||
// when the overview is opened, show every window actors (which
|
||||
// allows the blur to be shown too)
|
||||
this.connections.connect(
|
||||
Main.overview, 'showing',
|
||||
_ => this.window_map.forEach((meta_window, _pid) => {
|
||||
let window_actor = meta_window.get_compositor_private();
|
||||
window_actor.show();
|
||||
})
|
||||
);
|
||||
|
||||
// when the overview is closed, hide every actor that is not on the
|
||||
// current workspace (to mimic the original behaviour)
|
||||
this.connections.connect(
|
||||
Main.overview, 'hidden',
|
||||
_ => {
|
||||
let active_workspace =
|
||||
global.workspace_manager.get_active_workspace();
|
||||
|
||||
this.window_map.forEach((meta_window, _pid) => {
|
||||
let window_actor = meta_window.get_compositor_private();
|
||||
|
||||
if (
|
||||
meta_window.get_workspace() !== active_workspace
|
||||
)
|
||||
window_actor.hide();
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate through all existing windows and add blur as needed.
|
||||
update_all_windows() {
|
||||
// remove all previously blurred windows, in the case where the
|
||||
// whitelist was changed
|
||||
this.window_map.forEach(((_meta_window, pid) => {
|
||||
this.remove_blur(pid);
|
||||
}));
|
||||
|
||||
for (
|
||||
let i = 0;
|
||||
i < global.workspace_manager.get_n_workspaces();
|
||||
++i
|
||||
) {
|
||||
let workspace = global.workspace_manager.get_workspace_by_index(i);
|
||||
let windows = workspace.list_windows();
|
||||
|
||||
windows.forEach(meta_window => {
|
||||
let window_actor = meta_window.get_compositor_private();
|
||||
|
||||
// disconnect previous signals
|
||||
this.connections.disconnect_all_for(window_actor);
|
||||
|
||||
this.track_new(window_actor, meta_window);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the needed signals to every new tracked window, and adds blur if
|
||||
/// needed.
|
||||
track_new(window_actor, meta_window) {
|
||||
let pid = ("" + Math.random()).slice(2, 16);
|
||||
|
||||
window_actor['blur_provider_pid'] = pid;
|
||||
meta_window['blur_provider_pid'] = pid;
|
||||
|
||||
// remove the blur when the window is destroyed
|
||||
this.connections.connect(window_actor, 'destroy', window_actor => {
|
||||
let pid = window_actor.blur_provider_pid;
|
||||
if (this.blur_actor_map.has(pid)) {
|
||||
this.remove_blur(pid);
|
||||
}
|
||||
this.window_map.delete(pid);
|
||||
});
|
||||
|
||||
// update the blur when mutter-hint or wm-class is changed
|
||||
for (const prop of ['mutter-hints', 'wm-class']) {
|
||||
this.connections.connect(
|
||||
meta_window,
|
||||
`notify::${prop}`,
|
||||
_ => {
|
||||
let pid = meta_window.blur_provider_pid;
|
||||
this._log(`${prop} changed for pid ${pid}`);
|
||||
|
||||
let window_actor = meta_window.get_compositor_private();
|
||||
this.check_blur(pid, window_actor, meta_window);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// update the position and size when the window size changes
|
||||
this.connections.connect(meta_window, 'size-changed', () => {
|
||||
if (this.blur_actor_map.has(pid)) {
|
||||
let allocation = this.compute_allocation(meta_window);
|
||||
let blur_actor = this.blur_actor_map.get(pid);
|
||||
blur_actor.x = allocation.x;
|
||||
blur_actor.y = allocation.y;
|
||||
blur_actor.width = allocation.width;
|
||||
blur_actor.height = allocation.height;
|
||||
}
|
||||
});
|
||||
|
||||
this.check_blur(pid, window_actor, meta_window);
|
||||
}
|
||||
|
||||
/// Checks if the given actor needs to be blurred.
|
||||
///
|
||||
/// In order to be blurred, a window either:
|
||||
/// - is whitelisted in the user preferences if not enable-all
|
||||
/// - is not blacklisted if enable-all
|
||||
/// - has a correct mutter hint, set to `blur-provider=sigma_value`
|
||||
check_blur(pid, window_actor, meta_window) {
|
||||
let mutter_hint = meta_window.get_mutter_hints();
|
||||
let window_wm_class = meta_window.get_wm_class();
|
||||
|
||||
let enable_all = this.settings.applications.ENABLE_ALL;
|
||||
let whitelist = this.settings.applications.WHITELIST;
|
||||
let blacklist = this.settings.applications.BLACKLIST;
|
||||
|
||||
this._log(`checking blur for ${pid}`);
|
||||
|
||||
// either the window is included in whitelist
|
||||
if (window_wm_class !== ""
|
||||
&& ((enable_all && !blacklist.includes(window_wm_class))
|
||||
|| (!enable_all && whitelist.includes(window_wm_class))
|
||||
)
|
||||
&& [
|
||||
Meta.FrameType.NORMAL,
|
||||
Meta.FrameType.DIALOG,
|
||||
Meta.FrameType.MODAL_DIALOG
|
||||
].includes(meta_window.get_frame_type())
|
||||
) {
|
||||
this._log(`application ${pid} listed, blurring it`);
|
||||
|
||||
// get blur effect parameters
|
||||
|
||||
let brightness, sigma;
|
||||
|
||||
if (this.settings.applications.CUSTOMIZE) {
|
||||
brightness = this.settings.applications.BRIGHTNESS;
|
||||
sigma = this.settings.applications.SIGMA;
|
||||
} else {
|
||||
brightness = this.settings.BRIGHTNESS;
|
||||
sigma = this.settings.SIGMA;
|
||||
}
|
||||
|
||||
this.update_blur(pid, window_actor, meta_window, brightness, sigma);
|
||||
}
|
||||
|
||||
// or blur is asked by window itself
|
||||
else if (
|
||||
mutter_hint != null &&
|
||||
mutter_hint.includes("blur-provider")
|
||||
) {
|
||||
this._log(`application ${pid} has hint ${mutter_hint}, parsing`);
|
||||
|
||||
// get blur effect parameters
|
||||
let [brightness, sigma] = this.parse_xprop(mutter_hint);
|
||||
|
||||
this.update_blur(pid, window_actor, meta_window, brightness, sigma);
|
||||
}
|
||||
|
||||
// remove blur if the mutter hint is no longer valid, and the window
|
||||
// is not explicitly whitelisted or un-blacklisted
|
||||
else if (this.blur_actor_map.has(pid)) {
|
||||
this.remove_blur(pid);
|
||||
}
|
||||
}
|
||||
|
||||
/// When given the xprop property, returns the brightness and sigma values
|
||||
/// matching. If one of the two values is invalid, or missing, then it uses
|
||||
/// default values.
|
||||
///
|
||||
/// An xprop property is valid if it is in one of the following formats:
|
||||
///
|
||||
/// blur-provider=sigma:60,brightness:0.9
|
||||
/// blur-provider=s:10,brightness:0.492
|
||||
/// blur-provider=b:1.0,s:16
|
||||
///
|
||||
/// Brightness is a floating-point between 0.0 and 1.0 included.
|
||||
/// Sigma is an integer between 0 and 999 included.
|
||||
///
|
||||
/// If sigma is set to 0, then the blur is removed.
|
||||
/// Setting "default" instead of the two values will make the
|
||||
/// extension use its default value.
|
||||
///
|
||||
/// Note that no space can be inserted.
|
||||
///
|
||||
parse_xprop(property) {
|
||||
// set brightness and sigma to default values
|
||||
let brightness, sigma;
|
||||
if (this.settings.applications.CUSTOMIZE) {
|
||||
brightness = this.settings.applications.BRIGHTNESS;
|
||||
sigma = this.settings.applications.SIGMA;
|
||||
} else {
|
||||
brightness = this.settings.BRIGHTNESS;
|
||||
sigma = this.settings.SIGMA;
|
||||
}
|
||||
|
||||
// get the argument of the property
|
||||
let arg = property.match("blur-provider=(.*)");
|
||||
this._log(`argument = ${arg}`);
|
||||
|
||||
// if argument is valid, parse it
|
||||
if (arg != null) {
|
||||
// verify if there is only one value: in this case, this is sigma
|
||||
let maybe_sigma = parseInt(arg[1]);
|
||||
|
||||
if (
|
||||
!isNaN(maybe_sigma) &&
|
||||
maybe_sigma >= 0 &&
|
||||
maybe_sigma <= 999
|
||||
) {
|
||||
sigma = maybe_sigma;
|
||||
} else {
|
||||
// perform pattern matching
|
||||
let res_b = arg[1].match("(brightness|b):(default|0?1?\.[0-9]*)");
|
||||
let res_s = arg[1].match("(sigma|s):(default|\\d{1,3})");
|
||||
|
||||
// if values are valid and not default, change them to the xprop one
|
||||
if (
|
||||
res_b != null && res_b[2] !== 'default'
|
||||
) {
|
||||
brightness = parseFloat(res_b[2]);
|
||||
}
|
||||
|
||||
if (
|
||||
res_s != null && res_s[2] !== 'default'
|
||||
) {
|
||||
sigma = parseInt(res_s[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._log(`brightness = ${brightness}, sigma = ${sigma}`);
|
||||
|
||||
return [brightness, sigma];
|
||||
}
|
||||
|
||||
/// Updates the blur on a window which needs to be blurred.
|
||||
update_blur(pid, window_actor, meta_window, brightness, sigma) {
|
||||
// the window is already blurred, update its blur effect
|
||||
if (this.blur_actor_map.has(pid)) {
|
||||
// window is already blurred, but sigma is null: remove the blur
|
||||
if (sigma === 0) {
|
||||
this.remove_blur(pid);
|
||||
}
|
||||
// window is already blurred and sigma is non-null: update it
|
||||
else {
|
||||
this.update_blur_effect(
|
||||
this.blur_actor_map.get(pid),
|
||||
brightness,
|
||||
sigma
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// the window is not blurred, and sigma is a non-null value: blur it
|
||||
else if (sigma !== 0) {
|
||||
// window is not blurred, blur it
|
||||
this.create_blur_effect(
|
||||
pid,
|
||||
window_actor,
|
||||
meta_window,
|
||||
brightness,
|
||||
sigma
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Add the blur effect to the window.
|
||||
create_blur_effect(pid, window_actor, meta_window, brightness, sigma) {
|
||||
let blur_effect = new Shell.BlurEffect({
|
||||
sigma: sigma,
|
||||
brightness: brightness,
|
||||
mode: Shell.BlurMode.BACKGROUND
|
||||
});
|
||||
|
||||
let blur_actor = this.create_blur_actor(
|
||||
meta_window,
|
||||
window_actor,
|
||||
blur_effect
|
||||
);
|
||||
|
||||
// if hacks are selected, force to repaint the window
|
||||
if (this.settings.HACKS_LEVEL === 1 || this.settings.HACKS_LEVEL === 2) {
|
||||
this._log("applications hack level 1 or 2");
|
||||
|
||||
this.paint_signals.disconnect_all();
|
||||
this.paint_signals.connect(blur_actor, blur_effect);
|
||||
} else {
|
||||
this.paint_signals.disconnect_all();
|
||||
}
|
||||
|
||||
// insert the blurred widget
|
||||
window_actor.insert_child_at_index(blur_actor, 0);
|
||||
|
||||
// make sure window is blurred in overview
|
||||
if (this.settings.applications.BLUR_ON_OVERVIEW)
|
||||
this.enforce_window_visibility_on_overview_for(window_actor);
|
||||
|
||||
// set the window actor's opacity
|
||||
this.set_window_opacity(window_actor, this.settings.applications.OPACITY);
|
||||
|
||||
this.connections.connect(
|
||||
window_actor,
|
||||
'notify::opacity',
|
||||
_ => this.set_window_opacity(window_actor, this.settings.applications.OPACITY)
|
||||
);
|
||||
|
||||
// register the blur actor/effect
|
||||
blur_actor['blur_provider_pid'] = pid;
|
||||
this.blur_actor_map.set(pid, blur_actor);
|
||||
this.window_map.set(pid, meta_window);
|
||||
|
||||
// hide the blur if window is invisible
|
||||
if (!window_actor.visible) {
|
||||
blur_actor.hide();
|
||||
}
|
||||
|
||||
// hide the blur if window becomes invisible
|
||||
this.connections.connect(
|
||||
window_actor,
|
||||
'notify::visible',
|
||||
window_actor => {
|
||||
let pid = window_actor.blur_provider_pid;
|
||||
if (window_actor.visible) {
|
||||
this.blur_actor_map.get(pid).show();
|
||||
} else {
|
||||
this.blur_actor_map.get(pid).hide();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// Makes sure that, when the overview is visible, the window actor will
|
||||
/// stay visible no matter what.
|
||||
/// We can instead hide the last child of the window actor, which will
|
||||
/// improve performances without hiding the blur effect.
|
||||
enforce_window_visibility_on_overview_for(window_actor) {
|
||||
this.connections.connect(window_actor, 'notify::visible',
|
||||
_ => {
|
||||
if (this.settings.applications.BLUR_ON_OVERVIEW) {
|
||||
if (
|
||||
!window_actor.visible
|
||||
&& Main.overview.visible
|
||||
) {
|
||||
window_actor.show();
|
||||
window_actor.get_last_child().hide();
|
||||
}
|
||||
else if (
|
||||
window_actor.visible
|
||||
)
|
||||
window_actor.get_last_child().show();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// Set the opacity of the window actor that sits on top of the blur effect.
|
||||
set_window_opacity(window_actor, opacity) {
|
||||
window_actor.get_children().forEach(child => {
|
||||
if (child.name !== "blur-actor" && child.opacity != opacity)
|
||||
child.opacity = opacity;
|
||||
});
|
||||
}
|
||||
|
||||
/// Compute the size and position for a blur actor.
|
||||
/// On wayland, it seems like we need to divide by the scale to get the
|
||||
/// correct result.
|
||||
compute_allocation(meta_window) {
|
||||
const is_wayland = Meta.is_wayland_compositor();
|
||||
const monitor_index = meta_window.get_monitor();
|
||||
// check if the window is using wayland, or xwayland/xorg for rendering
|
||||
const scale = is_wayland && meta_window.get_client_type() == 0
|
||||
? Main.layoutManager.monitors[monitor_index].geometry_scale
|
||||
: 1;
|
||||
|
||||
let frame = meta_window.get_frame_rect();
|
||||
let buffer = meta_window.get_buffer_rect();
|
||||
|
||||
return {
|
||||
x: (frame.x - buffer.x) / scale,
|
||||
y: (frame.y - buffer.y) / scale,
|
||||
width: frame.width / scale,
|
||||
height: frame.height / scale
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns a new already blurred widget, configured to follow the size and
|
||||
/// position of its target window.
|
||||
create_blur_actor(meta_window, window_actor, blur_effect) {
|
||||
// compute the size and position
|
||||
let allocation = this.compute_allocation(meta_window);
|
||||
|
||||
// create the actor
|
||||
let blur_actor = new Clutter.Actor({
|
||||
x: allocation.x,
|
||||
y: allocation.y,
|
||||
width: allocation.width,
|
||||
height: allocation.height
|
||||
});
|
||||
|
||||
// add the effect
|
||||
blur_actor.add_effect_with_name('blur-effect', blur_effect);
|
||||
|
||||
return blur_actor;
|
||||
}
|
||||
|
||||
/// Updates the blur effect by overwriting its sigma and brightness values.
|
||||
update_blur_effect(blur_actor, brightness, sigma) {
|
||||
let effect = blur_actor.get_effect('blur-effect');
|
||||
effect.sigma = sigma;
|
||||
effect.brightness = brightness;
|
||||
}
|
||||
|
||||
/// Removes the blur actor from the shell and unregister it.
|
||||
remove_blur(pid) {
|
||||
this._log(`removing blur for pid ${pid}`);
|
||||
|
||||
let meta_window = this.window_map.get(pid);
|
||||
// disconnect needed signals and untrack window
|
||||
if (meta_window) {
|
||||
this.window_map.delete(pid);
|
||||
let window_actor = meta_window.get_compositor_private();
|
||||
|
||||
let blur_actor = this.blur_actor_map.get(pid);
|
||||
if (blur_actor) {
|
||||
this.blur_actor_map.delete(pid);
|
||||
|
||||
if (window_actor) {
|
||||
// reset the opacity
|
||||
this.set_window_opacity(window_actor, 255);
|
||||
|
||||
// remove the blurred actor
|
||||
window_actor.remove_child(blur_actor);
|
||||
|
||||
// disconnect the signals about overview animation etc
|
||||
this.connections.disconnect_all_for(window_actor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disable() {
|
||||
this._log("removing blur from applications...");
|
||||
|
||||
this.service?.unexport();
|
||||
|
||||
this.blur_actor_map.forEach(((_blur_actor, pid) => {
|
||||
this.remove_blur(pid);
|
||||
}));
|
||||
|
||||
this.connections.disconnect_all();
|
||||
this.paint_signals.disconnect_all();
|
||||
}
|
||||
|
||||
/// Update the opacity of all window actors.
|
||||
set_opacity() {
|
||||
let opacity = this.settings.applications.OPACITY;
|
||||
|
||||
this.window_map.forEach(((meta_window, _pid) => {
|
||||
let window_actor = meta_window.get_compositor_private();
|
||||
this.set_window_opacity(window_actor, opacity);
|
||||
}));
|
||||
}
|
||||
|
||||
/// Updates each blur effect to use new sigma value
|
||||
// FIXME set_sigma and set_brightness are called when the extension is
|
||||
// loaded and when sigma is changed, and do not respect the per-app
|
||||
// xprop behaviour
|
||||
set_sigma(s) {
|
||||
this.blur_actor_map.forEach((actor, _) => {
|
||||
actor.get_effect('blur-effect').set_sigma(s);
|
||||
});
|
||||
}
|
||||
|
||||
/// Updates each blur effect to use new brightness value
|
||||
set_brightness(b) {
|
||||
this.blur_actor_map.forEach((actor, _) => {
|
||||
actor.get_effect('blur-effect').set_brightness(b);
|
||||
});
|
||||
}
|
||||
|
||||
// not implemented for dynamic blur
|
||||
set_color(c) { }
|
||||
set_noise_amount(n) { }
|
||||
set_noise_lightness(l) { }
|
||||
|
||||
_log(str) {
|
||||
if (this.settings.DEBUG)
|
||||
console.log(`[Blur my Shell > applications] ${str}`);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,322 @@
|
|||
import St from 'gi://St';
|
||||
import Shell from 'gi://Shell';
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
const Signals = imports.signals;
|
||||
|
||||
import { PaintSignals } from '../effects/paint_signals.js';
|
||||
|
||||
const DASH_STYLES = [
|
||||
"transparent-dash",
|
||||
"light-dash",
|
||||
"dark-dash"
|
||||
];
|
||||
|
||||
|
||||
/// This type of object is created for every dash found, and talks to the main
|
||||
/// DashBlur thanks to signals.
|
||||
///
|
||||
/// This allows to dynamically track the created dashes for each screen.
|
||||
class DashInfos {
|
||||
constructor(dash_blur, dash, background_parent, effect, settings) {
|
||||
// the parent DashBlur object, to communicate
|
||||
this.dash_blur = dash_blur;
|
||||
// the blurred dash
|
||||
this.dash = dash;
|
||||
this.background_parent = background_parent;
|
||||
this.effect = effect;
|
||||
this.settings = settings;
|
||||
this.old_style = this.dash._background.style;
|
||||
|
||||
dash_blur.connections.connect(dash_blur, 'remove-dashes', () => {
|
||||
this._log("removing blur from dash");
|
||||
this.dash.get_parent().remove_child(this.background_parent);
|
||||
this.dash._background.style = this.old_style;
|
||||
|
||||
DASH_STYLES.forEach(
|
||||
style => this.dash.remove_style_class_name(style)
|
||||
);
|
||||
});
|
||||
|
||||
dash_blur.connections.connect(dash_blur, 'update-sigma', () => {
|
||||
this.effect.sigma = this.dash_blur.sigma;
|
||||
});
|
||||
|
||||
dash_blur.connections.connect(dash_blur, 'update-brightness', () => {
|
||||
this.effect.brightness = this.dash_blur.brightness;
|
||||
});
|
||||
|
||||
dash_blur.connections.connect(dash_blur, 'override-background', () => {
|
||||
this.dash._background.style = null;
|
||||
|
||||
DASH_STYLES.forEach(
|
||||
style => this.dash.remove_style_class_name(style)
|
||||
);
|
||||
|
||||
this.dash.set_style_class_name(
|
||||
DASH_STYLES[this.settings.dash_to_dock.STYLE_DASH_TO_DOCK]
|
||||
);
|
||||
});
|
||||
|
||||
dash_blur.connections.connect(dash_blur, 'reset-background', () => {
|
||||
this.dash._background.style = this.old_style;
|
||||
|
||||
DASH_STYLES.forEach(
|
||||
style => this.dash.remove_style_class_name(style)
|
||||
);
|
||||
});
|
||||
|
||||
dash_blur.connections.connect(dash_blur, 'show', () => {
|
||||
this.effect.sigma = this.dash_blur.sigma;
|
||||
});
|
||||
|
||||
dash_blur.connections.connect(dash_blur, 'hide', () => {
|
||||
this.effect.sigma = 0;
|
||||
});
|
||||
}
|
||||
|
||||
_log(str) {
|
||||
if (this.settings.DEBUG)
|
||||
console.log(`[Blur my Shell > dash] ${str}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const DashBlur = class DashBlur {
|
||||
constructor(connections, settings, _) {
|
||||
this.dashes = [];
|
||||
this.connections = connections;
|
||||
this.settings = settings;
|
||||
this.paint_signals = new PaintSignals(connections);
|
||||
this.sigma = this.settings.dash_to_dock.CUSTOMIZE
|
||||
? this.settings.dash_to_dock.SIGMA
|
||||
: this.settings.SIGMA;
|
||||
this.brightness = this.settings.dash_to_dock.CUSTOMIZE
|
||||
? this.settings.dash_to_dock.BRIGHTNESS
|
||||
: this.settings.BRIGHTNESS;
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
enable() {
|
||||
this.connections.connect(Main.uiGroup, 'actor-added', (_, actor) => {
|
||||
if (
|
||||
(actor.get_name() === "dashtodockContainer") &&
|
||||
(actor.constructor.name === 'DashToDock')
|
||||
)
|
||||
this.try_blur(actor);
|
||||
});
|
||||
|
||||
this.blur_existing_dashes();
|
||||
this.connect_to_overview();
|
||||
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
// Finds all existing dashes on every monitor, and call `try_blur` on them
|
||||
// We cannot only blur `Main.overview.dash`, as there could be several
|
||||
blur_existing_dashes() {
|
||||
this._log("searching for dash");
|
||||
|
||||
// blur every dash found, filtered by name
|
||||
Main.uiGroup.get_children().filter((child) => {
|
||||
return (child.get_name() === "dashtodockContainer") &&
|
||||
(child.constructor.name === 'DashToDock');
|
||||
}).forEach(this.try_blur.bind(this));
|
||||
}
|
||||
|
||||
// Tries to blur the dash contained in the given actor
|
||||
try_blur(dash_container) {
|
||||
let dash_box = dash_container._slider.get_child();
|
||||
|
||||
// verify that we did not already blur that dash
|
||||
if (!dash_box.get_children().some((child) => {
|
||||
return child.get_name() === "dash-blurred-background-parent";
|
||||
})) {
|
||||
this._log("dash to dock found, blurring it");
|
||||
|
||||
// finally blur the dash
|
||||
let dash = dash_box.get_children().find(child => {
|
||||
return child.get_name() === 'dash';
|
||||
});
|
||||
|
||||
this.dashes.push(this.blur_dash_from(dash, dash_container));
|
||||
}
|
||||
}
|
||||
|
||||
// Blurs the dash and returns a `DashInfos` containing its information
|
||||
blur_dash_from(dash, dash_container) {
|
||||
// the effect to be applied
|
||||
let effect = new Shell.BlurEffect({
|
||||
brightness: this.brightness,
|
||||
sigma: this.sigma,
|
||||
mode: Shell.BlurMode.BACKGROUND
|
||||
});
|
||||
|
||||
// dash background parent, not visible
|
||||
let background_parent = new St.Widget({
|
||||
name: 'dash-blurred-background-parent',
|
||||
style_class: 'dash-blurred-background-parent',
|
||||
width: 0,
|
||||
height: 0
|
||||
});
|
||||
|
||||
// dash background widget
|
||||
let background = new St.Widget({
|
||||
name: 'dash-blurred-background',
|
||||
style_class: 'dash-blurred-background',
|
||||
x: 0,
|
||||
y: dash_container._slider.y,
|
||||
width: dash.width,
|
||||
height: dash.height,
|
||||
});
|
||||
|
||||
// updates size and position on change
|
||||
this.connections.connect(dash_container._slider, 'notify::y', _ => {
|
||||
background.y = dash_container._slider.y;
|
||||
});
|
||||
this.connections.connect(dash, 'notify::width', _ => {
|
||||
background.width = dash.width;
|
||||
});
|
||||
this.connections.connect(dash, 'notify::height', _ => {
|
||||
background.height = dash.height;
|
||||
});
|
||||
|
||||
// add the widget to the dash
|
||||
background.add_effect(effect);
|
||||
background_parent.add_child(background);
|
||||
dash.get_parent().insert_child_at_index(background_parent, 0);
|
||||
|
||||
// HACK
|
||||
//
|
||||
//`Shell.BlurEffect` does not repaint when shadows are under it. [1]
|
||||
//
|
||||
// This does not entirely fix this bug (shadows caused by windows
|
||||
// still cause artifacts), but it prevents the shadows of the panel
|
||||
// buttons to cause artifacts on the panel itself
|
||||
//
|
||||
// [1]: https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/2857
|
||||
|
||||
if (this.settings.HACKS_LEVEL === 1) {
|
||||
this._log("dash hack level 1");
|
||||
this.paint_signals.disconnect_all();
|
||||
|
||||
let rp = () => {
|
||||
effect.queue_repaint();
|
||||
};
|
||||
|
||||
dash._box.get_children().forEach((icon) => {
|
||||
try {
|
||||
let zone = icon.get_child_at_index(0);
|
||||
|
||||
this.connections.connect(zone, [
|
||||
'enter-event', 'leave-event', 'button-press-event'
|
||||
], rp);
|
||||
} catch (e) {
|
||||
this._warn(`${e}, continuing`);
|
||||
}
|
||||
});
|
||||
|
||||
this.connections.connect(dash._box, 'actor-added', (_, actor) => {
|
||||
try {
|
||||
let zone = actor.get_child_at_index(0);
|
||||
|
||||
this.connections.connect(zone, [
|
||||
'enter-event', 'leave-event', 'button-press-event'
|
||||
], rp);
|
||||
} catch (e) {
|
||||
this._warn(`${e}, continuing`);
|
||||
}
|
||||
});
|
||||
|
||||
let show_apps = dash._showAppsIcon;
|
||||
|
||||
this.connections.connect(show_apps, [
|
||||
'enter-event', 'leave-event', 'button-press-event'
|
||||
], rp);
|
||||
|
||||
this.connections.connect(dash, 'leave-event', rp);
|
||||
} else if (this.settings.HACKS_LEVEL === 2) {
|
||||
this._log("dash hack level 2");
|
||||
|
||||
this.paint_signals.connect(background, effect);
|
||||
} else {
|
||||
this.paint_signals.disconnect_all();
|
||||
}
|
||||
|
||||
// create infos
|
||||
let infos = new DashInfos(
|
||||
this, dash, background_parent, effect, this.settings
|
||||
);
|
||||
|
||||
// update the background
|
||||
this.update_background();
|
||||
|
||||
// returns infos
|
||||
return infos;
|
||||
}
|
||||
|
||||
/// Connect when overview if opened/closed to hide/show the blur accordingly
|
||||
connect_to_overview() {
|
||||
this.connections.disconnect_all_for(Main.overview);
|
||||
|
||||
if (this.settings.dash_to_dock.UNBLUR_IN_OVERVIEW) {
|
||||
this.connections.connect(
|
||||
Main.overview, 'showing', this.hide.bind(this)
|
||||
);
|
||||
this.connections.connect(
|
||||
Main.overview, 'hidden', this.show.bind(this)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/// Updates the background to either remove it or not, according to the
|
||||
/// user preferences.
|
||||
update_background() {
|
||||
if (this.settings.dash_to_dock.OVERRIDE_BACKGROUND)
|
||||
this.emit('override-background', true);
|
||||
else
|
||||
this.emit('reset-background', true);
|
||||
}
|
||||
|
||||
set_sigma(sigma) {
|
||||
this.sigma = sigma;
|
||||
this.emit('update-sigma', true);
|
||||
}
|
||||
|
||||
set_brightness(brightness) {
|
||||
this.brightness = brightness;
|
||||
this.emit('update-brightness', true);
|
||||
}
|
||||
|
||||
// not implemented for dynamic blur
|
||||
set_color(c) { }
|
||||
set_noise_amount(n) { }
|
||||
set_noise_lightness(l) { }
|
||||
|
||||
disable() {
|
||||
this._log("removing blur from dashes");
|
||||
|
||||
this.emit('remove-dashes', true);
|
||||
|
||||
this.dashes = [];
|
||||
this.connections.disconnect_all();
|
||||
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
show() {
|
||||
this.emit('show', true);
|
||||
}
|
||||
hide() {
|
||||
this.emit('hide', true);
|
||||
}
|
||||
|
||||
_log(str) {
|
||||
if (this.settings.DEBUG)
|
||||
console.log(`[Blur my Shell > dash manager] ${str}`);
|
||||
}
|
||||
|
||||
_warn(str) {
|
||||
console.warn(`[Blur my Shell > dash manager] ${str}`);
|
||||
}
|
||||
};
|
||||
|
||||
Signals.addSignalMethods(DashBlur.prototype);
|
|
@ -0,0 +1,167 @@
|
|||
import St from 'gi://St';
|
||||
import Shell from 'gi://Shell';
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
import * as Background from 'resource:///org/gnome/shell/ui/background.js';
|
||||
import { UnlockDialog } from 'resource:///org/gnome/shell/ui/unlockDialog.js';
|
||||
|
||||
let sigma;
|
||||
let brightness;
|
||||
let color;
|
||||
let noise;
|
||||
let lightness;
|
||||
|
||||
const original_createBackground =
|
||||
UnlockDialog.prototype._createBackground;
|
||||
const original_updateBackgroundEffects =
|
||||
UnlockDialog.prototype._updateBackgroundEffects;
|
||||
|
||||
|
||||
export const LockscreenBlur = class LockscreenBlur {
|
||||
constructor(connections, settings, effects_manager) {
|
||||
this.connections = connections;
|
||||
this.settings = settings;
|
||||
this.effects_manager = effects_manager;
|
||||
}
|
||||
|
||||
enable() {
|
||||
this._log("blurring lockscreen");
|
||||
|
||||
brightness = this.settings.lockscreen.CUSTOMIZE
|
||||
? this.settings.lockscreen.BRIGHTNESS
|
||||
: this.settings.BRIGHTNESS;
|
||||
sigma = this.settings.lockscreen.CUSTOMIZE
|
||||
? this.settings.lockscreen.SIGMA
|
||||
: this.settings.SIGMA;
|
||||
color = this.settings.lockscreen.CUSTOMIZE
|
||||
? this.settings.lockscreen.COLOR
|
||||
: this.settings.COLOR;
|
||||
noise = this.settings.lockscreen.CUSTOMIZE
|
||||
? this.settings.lockscreen.NOISE_AMOUNT
|
||||
: this.settings.NOISE_AMOUNT;
|
||||
lightness = this.settings.lockscreen.CUSTOMIZE
|
||||
? this.settings.lockscreen.NOISE_LIGHTNESS
|
||||
: this.settings.NOISE_LIGHTNESS;
|
||||
|
||||
this.update_lockscreen();
|
||||
}
|
||||
|
||||
update_lockscreen() {
|
||||
UnlockDialog.prototype._createBackground =
|
||||
this._createBackground;
|
||||
UnlockDialog.prototype._updateBackgroundEffects =
|
||||
this._updateBackgroundEffects;
|
||||
}
|
||||
|
||||
_createBackground(monitorIndex) {
|
||||
let monitor = Main.layoutManager.monitors[monitorIndex];
|
||||
let widget = new St.Widget({
|
||||
style_class: "screen-shield-background",
|
||||
x: monitor.x,
|
||||
y: monitor.y,
|
||||
width: monitor.width,
|
||||
height: monitor.height,
|
||||
});
|
||||
|
||||
let blur_effect = new Shell.BlurEffect({
|
||||
name: 'blur',
|
||||
sigma: sigma,
|
||||
brightness: brightness
|
||||
});
|
||||
|
||||
// store the scale in the effect in order to retrieve later
|
||||
blur_effect.scale = monitor.geometry_scale;
|
||||
|
||||
let color_effect = global.blur_my_shell._lockscreen_blur.effects_manager.new_color_effect({
|
||||
name: 'color',
|
||||
color: color
|
||||
}, this.settings);
|
||||
|
||||
let noise_effect = global.blur_my_shell._lockscreen_blur.effects_manager.new_noise_effect({
|
||||
name: 'noise',
|
||||
noise: noise,
|
||||
lightness: lightness
|
||||
}, this.settings);
|
||||
|
||||
widget.add_effect(color_effect);
|
||||
widget.add_effect(noise_effect);
|
||||
widget.add_effect(blur_effect);
|
||||
|
||||
let bgManager = new Background.BackgroundManager({
|
||||
container: widget,
|
||||
monitorIndex,
|
||||
controlPosition: false,
|
||||
});
|
||||
|
||||
this._bgManagers.push(bgManager);
|
||||
|
||||
this._backgroundGroup.add_child(widget);
|
||||
}
|
||||
|
||||
_updateBackgroundEffects() {
|
||||
for (const widget of this._backgroundGroup) {
|
||||
const color_effect = widget.get_effect('color');
|
||||
const noise_effect = widget.get_effect('noise');
|
||||
const blur_effect = widget.get_effect('blur');
|
||||
|
||||
if (color_effect)
|
||||
color_effect.set({
|
||||
color: color
|
||||
});
|
||||
|
||||
if (noise_effect) {
|
||||
noise_effect.set({
|
||||
noise: noise,
|
||||
lightness: lightness,
|
||||
});
|
||||
}
|
||||
|
||||
if (blur_effect) {
|
||||
blur_effect.set({
|
||||
brightness: brightness,
|
||||
sigma: sigma * blur_effect.scale,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set_sigma(s) {
|
||||
sigma = s;
|
||||
this.update_lockscreen();
|
||||
}
|
||||
|
||||
set_brightness(b) {
|
||||
brightness = b;
|
||||
this.update_lockscreen();
|
||||
}
|
||||
|
||||
set_color(c) {
|
||||
color = c;
|
||||
this.update_lockscreen();
|
||||
}
|
||||
|
||||
set_noise_amount(n) {
|
||||
noise = n;
|
||||
this.update_lockscreen();
|
||||
}
|
||||
|
||||
set_noise_lightness(l) {
|
||||
lightness = l;
|
||||
this.update_lockscreen();
|
||||
}
|
||||
|
||||
disable() {
|
||||
this._log("removing blur from lockscreen");
|
||||
|
||||
UnlockDialog.prototype._createBackground =
|
||||
original_createBackground;
|
||||
UnlockDialog.prototype._updateBackgroundEffects =
|
||||
original_updateBackgroundEffects;
|
||||
|
||||
this.connections.disconnect_all();
|
||||
}
|
||||
|
||||
_log(str) {
|
||||
if (this.settings.DEBUG)
|
||||
console.log(`[Blur my Shell > lockscreen] ${str}`);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,309 @@
|
|||
import Shell from 'gi://Shell';
|
||||
import Meta from 'gi://Meta';
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
|
||||
import { WorkspaceAnimationController } from 'resource:///org/gnome/shell/ui/workspaceAnimation.js';
|
||||
const wac_proto = WorkspaceAnimationController.prototype;
|
||||
|
||||
const OVERVIEW_COMPONENTS_STYLE = [
|
||||
"",
|
||||
"overview-components-light",
|
||||
"overview-components-dark",
|
||||
"overview-components-transparent"
|
||||
];
|
||||
|
||||
|
||||
export const OverviewBlur = class OverviewBlur {
|
||||
constructor(connections, settings, effects_manager) {
|
||||
this.connections = connections;
|
||||
this.effects = [];
|
||||
this.settings = settings;
|
||||
this.effects_manager = effects_manager;
|
||||
this._workspace_switch_bg_actors = [];
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
enable() {
|
||||
this._log("blurring overview");
|
||||
|
||||
// connect to every background change (even without changing image)
|
||||
// FIXME this signal is fired very often, so we should find another one
|
||||
// fired only when necessary (but that still catches all cases)
|
||||
this.connections.connect(
|
||||
Main.layoutManager._backgroundGroup,
|
||||
'notify',
|
||||
_ => {
|
||||
this._log("updated background");
|
||||
this.update_backgrounds();
|
||||
}
|
||||
);
|
||||
|
||||
// connect to monitors change
|
||||
this.connections.connect(
|
||||
Main.layoutManager,
|
||||
'monitors-changed',
|
||||
_ => {
|
||||
if (Main.screenShield && !Main.screenShield.locked) {
|
||||
this._log("changed monitors");
|
||||
this.update_backgrounds();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// add css class name for workspace-switch background
|
||||
Main.uiGroup.add_style_class_name("blurred-overview");
|
||||
|
||||
// add css class name to make components semi-transparent if wanted
|
||||
this.update_components_classname();
|
||||
|
||||
// update backgrounds when the component is enabled
|
||||
this.update_backgrounds();
|
||||
|
||||
|
||||
// part for the workspace animation switch
|
||||
|
||||
// make sure not to do this part if the extension was enabled prior, as
|
||||
// the functions would call themselves and cause infinite recursion
|
||||
if (!this.enabled) {
|
||||
// store original workspace switching methods for restoring them on
|
||||
// disable()
|
||||
this._original_PrepareSwitch = wac_proto._prepareWorkspaceSwitch;
|
||||
this._original_FinishSwitch = wac_proto._finishWorkspaceSwitch;
|
||||
|
||||
const w_m = global.workspace_manager;
|
||||
const outer_this = this;
|
||||
|
||||
// create a blurred background actor for each monitor during a
|
||||
// workspace switch
|
||||
wac_proto._prepareWorkspaceSwitch = function (...params) {
|
||||
outer_this._log("prepare workspace switch");
|
||||
outer_this._original_PrepareSwitch.apply(this, params);
|
||||
|
||||
// this permits to show the blur behind windows that are on
|
||||
// workspaces on the left and right
|
||||
if (
|
||||
outer_this.settings.applications.BLUR
|
||||
) {
|
||||
let ws_index = w_m.get_active_workspace_index();
|
||||
[ws_index - 1, ws_index + 1].forEach(
|
||||
i => w_m.get_workspace_by_index(i)?.list_windows().forEach(
|
||||
window => window.get_compositor_private().show()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Main.layoutManager.monitors.forEach(monitor => {
|
||||
if (
|
||||
!(
|
||||
Meta.prefs_get_workspaces_only_on_primary() &&
|
||||
(monitor !== Main.layoutManager.primaryMonitor)
|
||||
)
|
||||
) {
|
||||
const bg_actor = outer_this.create_background_actor(
|
||||
monitor, true
|
||||
);
|
||||
|
||||
Main.uiGroup.insert_child_above(
|
||||
bg_actor,
|
||||
global.window_group
|
||||
);
|
||||
|
||||
// store the actors so that we can delete them later
|
||||
outer_this._workspace_switch_bg_actors.push(bg_actor);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// remove the workspace-switch actors when the switch is done
|
||||
wac_proto._finishWorkspaceSwitch = function (...params) {
|
||||
outer_this._log("finish workspace switch");
|
||||
outer_this._original_FinishSwitch.apply(this, params);
|
||||
|
||||
// this hides windows that are not on the current workspace
|
||||
if (
|
||||
outer_this.settings.applications.BLUR
|
||||
)
|
||||
for (let i = 0; i < w_m.get_n_workspaces(); i++) {
|
||||
if (i != w_m.get_active_workspace_index())
|
||||
w_m.get_workspace_by_index(i)?.list_windows().forEach(
|
||||
window => window.get_compositor_private().hide()
|
||||
);
|
||||
}
|
||||
|
||||
outer_this.effects = outer_this.effects.filter(
|
||||
effects_group => !effects_group.is_transition
|
||||
);
|
||||
|
||||
outer_this._workspace_switch_bg_actors.forEach(actor => {
|
||||
actor.destroy();
|
||||
});
|
||||
outer_this._workspace_switch_bg_actors = [];
|
||||
};
|
||||
}
|
||||
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
update_backgrounds() {
|
||||
// remove every old background
|
||||
this.remove_background_actors();
|
||||
|
||||
// add new backgrounds
|
||||
Main.layoutManager.monitors.forEach(monitor => {
|
||||
const bg_actor = this.create_background_actor(monitor, false);
|
||||
|
||||
Main.layoutManager.overviewGroup.insert_child_at_index(
|
||||
bg_actor,
|
||||
monitor.index
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
create_background_actor(monitor, is_transition) {
|
||||
let bg_actor = new Meta.BackgroundActor({
|
||||
meta_display: global.display,
|
||||
monitor: monitor.index
|
||||
});
|
||||
let background_group = Main.layoutManager._backgroundGroup
|
||||
.get_children()
|
||||
.filter((child) => child instanceof Meta.BackgroundActor);
|
||||
let background =
|
||||
background_group[
|
||||
Main.layoutManager.monitors.length - monitor.index - 1
|
||||
];
|
||||
|
||||
if (!background) {
|
||||
this._warn("could not get background for overview");
|
||||
return bg_actor;
|
||||
}
|
||||
|
||||
bg_actor.content.set({
|
||||
background: background.get_content().background
|
||||
});
|
||||
|
||||
let blur_effect = new Shell.BlurEffect({
|
||||
brightness: this.settings.overview.CUSTOMIZE
|
||||
? this.settings.overview.BRIGHTNESS
|
||||
: this.settings.BRIGHTNESS,
|
||||
sigma: this.settings.overview.CUSTOMIZE
|
||||
? this.settings.overview.SIGMA
|
||||
: this.settings.SIGMA
|
||||
* monitor.geometry_scale,
|
||||
mode: Shell.BlurMode.ACTOR
|
||||
});
|
||||
|
||||
// store the scale in the effect in order to retrieve it in set_sigma
|
||||
blur_effect.scale = monitor.geometry_scale;
|
||||
|
||||
let color_effect = this.effects_manager.new_color_effect({
|
||||
color: this.settings.overview.CUSTOMIZE
|
||||
? this.settings.overview.COLOR
|
||||
: this.settings.COLOR
|
||||
}, this.settings);
|
||||
|
||||
let noise_effect = this.effects_manager.new_noise_effect({
|
||||
noise: this.settings.overview.CUSTOMIZE
|
||||
? this.settings.overview.NOISE_AMOUNT
|
||||
: this.settings.NOISE_AMOUNT,
|
||||
lightness: this.settings.overview.CUSTOMIZE
|
||||
? this.settings.overview.NOISE_LIGHTNESS
|
||||
: this.settings.NOISE_LIGHTNESS
|
||||
}, this.settings);
|
||||
|
||||
bg_actor.add_effect(color_effect);
|
||||
bg_actor.add_effect(noise_effect);
|
||||
bg_actor.add_effect(blur_effect);
|
||||
this.effects.push({ blur_effect, color_effect, noise_effect, is_transition });
|
||||
|
||||
bg_actor.set_x(monitor.x);
|
||||
bg_actor.set_y(monitor.y);
|
||||
|
||||
return bg_actor;
|
||||
}
|
||||
|
||||
/// Updates the classname to style overview components with semi-transparent
|
||||
/// backgrounds.
|
||||
update_components_classname() {
|
||||
OVERVIEW_COMPONENTS_STYLE.forEach(
|
||||
style => Main.uiGroup.remove_style_class_name(style)
|
||||
);
|
||||
|
||||
Main.uiGroup.add_style_class_name(
|
||||
OVERVIEW_COMPONENTS_STYLE[this.settings.overview.STYLE_COMPONENTS]
|
||||
);
|
||||
}
|
||||
|
||||
set_sigma(s) {
|
||||
this.effects.forEach(effect => {
|
||||
effect.blur_effect.sigma = s * effect.blur_effect.scale;
|
||||
});
|
||||
}
|
||||
|
||||
set_brightness(b) {
|
||||
this.effects.forEach(effect => {
|
||||
effect.blur_effect.brightness = b;
|
||||
});
|
||||
}
|
||||
|
||||
set_color(c) {
|
||||
this.effects.forEach(effect => {
|
||||
effect.color_effect.color = c;
|
||||
});
|
||||
}
|
||||
|
||||
set_noise_amount(n) {
|
||||
this.effects.forEach(effect => {
|
||||
effect.noise_effect.noise = n;
|
||||
});
|
||||
}
|
||||
|
||||
set_noise_lightness(l) {
|
||||
this.effects.forEach(effect => {
|
||||
effect.noise_effect.lightness = l;
|
||||
});
|
||||
}
|
||||
|
||||
remove_background_actors() {
|
||||
Main.layoutManager.overviewGroup.get_children().forEach(actor => {
|
||||
if (actor.constructor.name === 'Meta_BackgroundActor') {
|
||||
actor.get_effects().forEach(effect => {
|
||||
this.effects_manager.remove(effect);
|
||||
});
|
||||
Main.layoutManager.overviewGroup.remove_child(actor);
|
||||
actor.destroy();
|
||||
}
|
||||
});
|
||||
this.effects = [];
|
||||
}
|
||||
|
||||
disable() {
|
||||
this._log("removing blur from overview");
|
||||
this.remove_background_actors();
|
||||
Main.uiGroup.remove_style_class_name("blurred-overview");
|
||||
OVERVIEW_COMPONENTS_STYLE.forEach(
|
||||
style => Main.uiGroup.remove_style_class_name(style)
|
||||
);
|
||||
|
||||
// make sure to absolutely not do this if the component was not enabled
|
||||
// prior, as this would cause infinite recursion
|
||||
if (this.enabled) {
|
||||
// restore original behavior
|
||||
if (this._original_PrepareSwitch)
|
||||
wac_proto._prepareWorkspaceSwitch = this._original_PrepareSwitch;
|
||||
if (this._original_FinishSwitch)
|
||||
wac_proto._finishWorkspaceSwitch = this._original_FinishSwitch;
|
||||
}
|
||||
|
||||
this.connections.disconnect_all();
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
_log(str) {
|
||||
if (this.settings.DEBUG)
|
||||
console.log(`[Blur my Shell > overview] ${str}`);
|
||||
}
|
||||
|
||||
_warn(str) {
|
||||
console.warn(`[Blur my Shell > overview] ${str}`);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,695 @@
|
|||
import St from 'gi://St';
|
||||
import Shell from 'gi://Shell';
|
||||
import Meta from 'gi://Meta';
|
||||
import Mtk from 'gi://Mtk';
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
|
||||
import { PaintSignals } from '../effects/paint_signals.js';
|
||||
|
||||
const DASH_TO_PANEL_UUID = 'dash-to-panel@jderose9.github.com';
|
||||
const PANEL_STYLES = [
|
||||
"transparent-panel",
|
||||
"light-panel",
|
||||
"dark-panel",
|
||||
"contrasted-panel"
|
||||
];
|
||||
|
||||
|
||||
export const PanelBlur = class PanelBlur {
|
||||
constructor(connections, settings, effects_manager) {
|
||||
this.connections = connections;
|
||||
this.window_signal_ids = new Map();
|
||||
this.settings = settings;
|
||||
this.effects_manager = effects_manager;
|
||||
this.actors_list = [];
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
enable() {
|
||||
this._log("blurring top panel");
|
||||
|
||||
// check for panels when Dash to Panel is activated
|
||||
this.connections.connect(
|
||||
Main.extensionManager,
|
||||
'extension-state-changed',
|
||||
(_, extension) => {
|
||||
if (extension.uuid === DASH_TO_PANEL_UUID
|
||||
&& extension.state === 1
|
||||
) {
|
||||
this.connections.connect(
|
||||
global.dashToPanel,
|
||||
'panels-created',
|
||||
_ => this.blur_dtp_panels()
|
||||
);
|
||||
|
||||
this.blur_existing_panels();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.blur_existing_panels();
|
||||
|
||||
// connect to overview being opened/closed, and dynamically show or not
|
||||
// the blur when a window is near a panel
|
||||
this.connect_to_windows_and_overview();
|
||||
|
||||
// connect to every background change (even without changing image)
|
||||
// FIXME this signal is fired very often, so we should find another one
|
||||
// fired only when necessary (but that still catches all cases)
|
||||
this.connections.connect(
|
||||
Main.layoutManager._backgroundGroup,
|
||||
'notify',
|
||||
_ => this.actors_list.forEach(actors =>
|
||||
this.update_wallpaper(actors)
|
||||
)
|
||||
);
|
||||
|
||||
// connect to monitors change
|
||||
this.connections.connect(
|
||||
Main.layoutManager,
|
||||
'monitors-changed',
|
||||
_ => {
|
||||
if (Main.screenShield && !Main.screenShield.locked) {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this._log("resetting...");
|
||||
|
||||
this.disable();
|
||||
setTimeout(_ => this.enable(), 1);
|
||||
}
|
||||
|
||||
/// Check for already existing panels and blur them if they are not already
|
||||
blur_existing_panels() {
|
||||
// check if dash-to-panel is present
|
||||
if (global.dashToPanel) {
|
||||
// blur already existing ones
|
||||
if (global.dashToPanel.panels)
|
||||
this.blur_dtp_panels();
|
||||
} else {
|
||||
// if no dash-to-panel, blur the main and only panel
|
||||
this.maybe_blur_panel(Main.panel);
|
||||
}
|
||||
}
|
||||
|
||||
blur_dtp_panels() {
|
||||
// FIXME when Dash to Panel changes its size, it seems it creates new
|
||||
// panels; but I can't get to delete old widgets
|
||||
|
||||
// blur every panel found
|
||||
global.dashToPanel.panels.forEach(p => {
|
||||
this.maybe_blur_panel(p.panel);
|
||||
});
|
||||
|
||||
// if main panel is not included in the previous panels, blur it
|
||||
if (
|
||||
!global.dashToPanel.panels
|
||||
.map(p => p.panel)
|
||||
.includes(Main.panel)
|
||||
&&
|
||||
this.settings.dash_to_panel.BLUR_ORIGINAL_PANEL
|
||||
)
|
||||
this.maybe_blur_panel(Main.panel);
|
||||
};
|
||||
|
||||
/// Blur a panel only if it is not already blurred (contained in the list)
|
||||
maybe_blur_panel(panel) {
|
||||
// check if the panel is contained in the list
|
||||
let actors = this.actors_list.find(
|
||||
actors => actors.widgets.panel == panel
|
||||
);
|
||||
|
||||
if (!actors)
|
||||
// if the actors is not blurred, blur it
|
||||
this.blur_panel(panel);
|
||||
else
|
||||
// if it is blurred, update the blur anyway
|
||||
this.change_blur_type(actors);
|
||||
}
|
||||
|
||||
/// Blur a panel
|
||||
blur_panel(panel) {
|
||||
let panel_box = panel.get_parent();
|
||||
let is_dtp_panel = false;
|
||||
if (!panel_box.name) {
|
||||
is_dtp_panel = true;
|
||||
panel_box = panel_box.get_parent();
|
||||
}
|
||||
|
||||
let monitor = this.find_monitor_for(panel);
|
||||
if (!monitor)
|
||||
return;
|
||||
|
||||
let background_parent = new St.Widget({
|
||||
name: 'topbar-blurred-background-parent',
|
||||
x: 0, y: 0, width: 0, height: 0
|
||||
});
|
||||
|
||||
let background = this.settings.panel.STATIC_BLUR
|
||||
? new Meta.BackgroundActor({
|
||||
meta_display: global.display,
|
||||
monitor: monitor.index
|
||||
})
|
||||
: new St.Widget;
|
||||
|
||||
background_parent.add_child(background);
|
||||
|
||||
// insert background parent
|
||||
panel_box.insert_child_at_index(background_parent, 0);
|
||||
|
||||
let blur = new Shell.BlurEffect({
|
||||
brightness: this.settings.panel.CUSTOMIZE
|
||||
? this.settings.panel.BRIGHTNESS
|
||||
: this.settings.BRIGHTNESS,
|
||||
sigma: this.settings.panel.CUSTOMIZE
|
||||
? this.settings.panel.SIGMA
|
||||
: this.settings.SIGMA
|
||||
* monitor.geometry_scale,
|
||||
mode: this.settings.panel.STATIC_BLUR
|
||||
? Shell.BlurMode.ACTOR
|
||||
: Shell.BlurMode.BACKGROUND
|
||||
});
|
||||
|
||||
// store the scale in the effect in order to retrieve it in set_sigma
|
||||
blur.scale = monitor.geometry_scale;
|
||||
|
||||
let color = this.effects_manager.new_color_effect({
|
||||
color: this.settings.panel.CUSTOMIZE
|
||||
? this.settings.panel.COLOR
|
||||
: this.settings.COLOR
|
||||
}, this.settings);
|
||||
|
||||
let noise = this.effects_manager.new_noise_effect({
|
||||
noise: this.settings.panel.CUSTOMIZE
|
||||
? this.settings.panel.NOISE_AMOUNT
|
||||
: this.settings.NOISE_AMOUNT,
|
||||
lightness: this.settings.panel.CUSTOMIZE
|
||||
? this.settings.panel.NOISE_LIGHTNESS
|
||||
: this.settings.NOISE_LIGHTNESS
|
||||
}, this.settings);
|
||||
|
||||
let paint_signals = new PaintSignals(this.connections);
|
||||
|
||||
let actors = {
|
||||
widgets: { panel, panel_box, background, background_parent },
|
||||
effects: { blur, color, noise },
|
||||
paint_signals,
|
||||
monitor,
|
||||
is_dtp_panel
|
||||
};
|
||||
|
||||
this.actors_list.push(actors);
|
||||
|
||||
// perform updates
|
||||
this.change_blur_type(actors);
|
||||
|
||||
// connect to panel, panel_box and its parent position or size change
|
||||
// this should fire update_size every time one of its params change
|
||||
this.connections.connect(
|
||||
panel,
|
||||
'notify::position',
|
||||
_ => this.update_size(actors)
|
||||
);
|
||||
this.connections.connect(
|
||||
panel_box,
|
||||
['notify::size', 'notify::position'],
|
||||
_ => this.update_size(actors)
|
||||
);
|
||||
this.connections.connect(
|
||||
panel_box.get_parent(),
|
||||
'notify::position',
|
||||
_ => this.update_size(actors)
|
||||
);
|
||||
}
|
||||
|
||||
update_all_blur_type() {
|
||||
this.actors_list.forEach(actors => this.change_blur_type(actors));
|
||||
}
|
||||
|
||||
change_blur_type(actors) {
|
||||
let is_static = this.settings.panel.STATIC_BLUR;
|
||||
|
||||
// reset widgets to right state
|
||||
actors.widgets.background_parent.remove_child(actors.widgets.background);
|
||||
this.effects_manager.remove(actors.effects.blur);
|
||||
this.effects_manager.remove(actors.effects.color);
|
||||
this.effects_manager.remove(actors.effects.noise);
|
||||
|
||||
// create new background actor
|
||||
actors.widgets.background = is_static
|
||||
? new Meta.BackgroundActor({
|
||||
meta_display: global.display,
|
||||
monitor: this.find_monitor_for(actors.widgets.panel).index
|
||||
})
|
||||
: new St.Widget;
|
||||
|
||||
// change blur mode
|
||||
actors.effects.blur.set_mode(is_static ? 0 : 1);
|
||||
|
||||
// disable other effects if the blur is dynamic, as they makes it opaque
|
||||
actors.effects.color._static = is_static;
|
||||
actors.effects.noise._static = is_static;
|
||||
actors.effects.color.update_enabled();
|
||||
actors.effects.noise.update_enabled();
|
||||
|
||||
// add the effects in order
|
||||
actors.widgets.background.add_effect(actors.effects.color);
|
||||
actors.widgets.background.add_effect(actors.effects.noise);
|
||||
actors.widgets.background.add_effect(actors.effects.blur);
|
||||
|
||||
// add the background actor behing the panel
|
||||
actors.widgets.background_parent.add_child(actors.widgets.background);
|
||||
|
||||
// perform updates
|
||||
this.update_wallpaper(actors);
|
||||
this.update_size(actors);
|
||||
|
||||
|
||||
// HACK
|
||||
//
|
||||
//`Shell.BlurEffect` does not repaint when shadows are under it. [1]
|
||||
//
|
||||
// This does not entirely fix this bug (shadows caused by windows
|
||||
// still cause artifacts), but it prevents the shadows of the panel
|
||||
// buttons to cause artifacts on the panel itself
|
||||
//
|
||||
// [1]: https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/2857
|
||||
|
||||
if (!is_static) {
|
||||
if (this.settings.HACKS_LEVEL === 1) {
|
||||
this._log("panel hack level 1");
|
||||
actors.paint_signals.disconnect_all();
|
||||
|
||||
let rp = () => { actors.effects.blur.queue_repaint(); };
|
||||
|
||||
this.connections.connect(actors.widgets.panel, [
|
||||
'enter-event', 'leave-event', 'button-press-event'
|
||||
], rp);
|
||||
|
||||
actors.widgets.panel.get_children().forEach(child => {
|
||||
this.connections.connect(child, [
|
||||
'enter-event', 'leave-event', 'button-press-event'
|
||||
], rp);
|
||||
});
|
||||
} else if (this.settings.HACKS_LEVEL === 2) {
|
||||
this._log("panel hack level 2");
|
||||
actors.paint_signals.disconnect_all();
|
||||
|
||||
actors.paint_signals.connect(
|
||||
actors.widgets.background, actors.effects.blur
|
||||
);
|
||||
} else {
|
||||
actors.paint_signals.disconnect_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update_wallpaper(actors) {
|
||||
// if static blur, get right wallpaper and update blur with it
|
||||
if (this.settings.panel.STATIC_BLUR) {
|
||||
let bg = Main.layoutManager._backgroundGroup.get_child_at_index(
|
||||
Main.layoutManager.monitors.length
|
||||
- this.find_monitor_for(actors.widgets.panel).index - 1
|
||||
);
|
||||
if (bg)
|
||||
actors.widgets.background.content.set({
|
||||
background: bg.get_content().background
|
||||
});
|
||||
else
|
||||
this._warn("could not get background for panel");
|
||||
}
|
||||
}
|
||||
|
||||
update_size(actors) {
|
||||
let panel = actors.widgets.panel;
|
||||
let panel_box = actors.widgets.panel_box;
|
||||
let background = actors.widgets.background;
|
||||
let monitor = this.find_monitor_for(panel);
|
||||
if (!monitor)
|
||||
return;
|
||||
|
||||
let [width, height] = panel_box.get_size();
|
||||
background.width = width;
|
||||
background.height = height;
|
||||
|
||||
// if static blur, need to clip the background
|
||||
if (this.settings.panel.STATIC_BLUR) {
|
||||
// an alternative to panel.get_transformed_position, because it
|
||||
// sometimes yields NaN (probably when the actor is not fully
|
||||
// positionned yet)
|
||||
let [p_x, p_y] = panel_box.get_position();
|
||||
let [p_p_x, p_p_y] = panel_box.get_parent().get_position();
|
||||
let x = p_x + p_p_x - monitor.x;
|
||||
let y = p_y + p_p_y - monitor.y;
|
||||
|
||||
background.set_clip(x, y, width, height);
|
||||
background.x = -x;
|
||||
background.y = -y;
|
||||
|
||||
// fixes a bug where the blur is washed away when changing the sigma
|
||||
this.invalidate_blur(actors);
|
||||
} else {
|
||||
background.x = panel.x;
|
||||
background.y = panel.y;
|
||||
}
|
||||
|
||||
// update the monitor panel is on
|
||||
actors.monitor = this.find_monitor_for(panel);
|
||||
}
|
||||
|
||||
/// An helper function to find the monitor in which an actor is situated,
|
||||
/// there might be a pre-existing function in GLib already
|
||||
find_monitor_for(actor) {
|
||||
let extents = actor.get_transformed_extents();
|
||||
let rect = new Mtk.Rectangle({
|
||||
x: extents.get_x(),
|
||||
y: extents.get_y(),
|
||||
width: extents.get_width(),
|
||||
height: extents.get_height(),
|
||||
});
|
||||
|
||||
let index = global.display.get_monitor_index_for_rect(rect);
|
||||
|
||||
return Main.layoutManager.monitors[index];
|
||||
}
|
||||
|
||||
/// Connect when overview if opened/closed to hide/show the blur accordingly
|
||||
///
|
||||
/// If HIDETOPBAR is set, we need just to hide the blur when showing appgrid
|
||||
/// (so no shadow is cropped)
|
||||
connect_to_overview() {
|
||||
// may be called when panel blur is disabled, if hidetopbar
|
||||
// compatibility is toggled on/off
|
||||
// if this is the case, do nothing as only the panel blur interfers with
|
||||
// hidetopbar
|
||||
if (
|
||||
this.settings.panel.BLUR &&
|
||||
this.settings.panel.UNBLUR_IN_OVERVIEW
|
||||
) {
|
||||
if (!this.settings.hidetopbar.COMPATIBILITY) {
|
||||
this.connections.connect(
|
||||
Main.overview, 'showing', this.hide.bind(this)
|
||||
);
|
||||
this.connections.connect(
|
||||
Main.overview, 'hidden', this.show.bind(this)
|
||||
);
|
||||
} else {
|
||||
let appDisplay = Main.overview._overview._controls._appDisplay;
|
||||
|
||||
this.connections.connect(
|
||||
appDisplay, 'show', this.hide.bind(this)
|
||||
);
|
||||
this.connections.connect(
|
||||
appDisplay, 'hide', this.show.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to windows disable transparency when a window is too close
|
||||
connect_to_windows() {
|
||||
if (
|
||||
this.settings.panel.OVERRIDE_BACKGROUND_DYNAMICALLY
|
||||
) {
|
||||
// connect to overview opening/closing
|
||||
this.connections.connect(Main.overview, ['showing', 'hiding'],
|
||||
this.update_visibility.bind(this)
|
||||
);
|
||||
|
||||
// connect to session mode update
|
||||
this.connections.connect(Main.sessionMode, 'updated',
|
||||
this.update_visibility.bind(this)
|
||||
);
|
||||
|
||||
// manage already-existing windows
|
||||
for (const meta_window_actor of global.get_window_actors()) {
|
||||
this.on_window_actor_added(
|
||||
meta_window_actor.get_parent(), meta_window_actor
|
||||
);
|
||||
}
|
||||
|
||||
// manage windows at their creation/removal
|
||||
this.connections.connect(global.window_group, 'actor-added',
|
||||
this.on_window_actor_added.bind(this)
|
||||
);
|
||||
this.connections.connect(global.window_group, 'actor-removed',
|
||||
this.on_window_actor_removed.bind(this)
|
||||
);
|
||||
|
||||
// connect to a workspace change
|
||||
this.connections.connect(global.window_manager, 'switch-workspace',
|
||||
this.update_visibility.bind(this)
|
||||
);
|
||||
|
||||
// perform early update
|
||||
this.update_visibility();
|
||||
} else {
|
||||
// reset transparency for every panels
|
||||
this.actors_list.forEach(
|
||||
actors => this.set_should_override_panel(actors, true)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An helper to connect to both the windows and overview signals.
|
||||
/// This is the only function that should be directly called, to prevent
|
||||
/// inconsistencies with signals not being disconnected.
|
||||
connect_to_windows_and_overview() {
|
||||
this.disconnect_from_windows_and_overview();
|
||||
this.connect_to_overview();
|
||||
this.connect_to_windows();
|
||||
}
|
||||
|
||||
/// Disconnect all the connections created by connect_to_windows
|
||||
disconnect_from_windows_and_overview() {
|
||||
// disconnect the connections to actors
|
||||
for (const actor of [
|
||||
Main.overview, Main.sessionMode,
|
||||
global.window_group, global.window_manager,
|
||||
Main.overview._overview._controls._appDisplay
|
||||
]) {
|
||||
this.connections.disconnect_all_for(actor);
|
||||
}
|
||||
|
||||
// disconnect the connections from windows
|
||||
for (const [actor, ids] of this.window_signal_ids) {
|
||||
for (const id of ids) {
|
||||
actor.disconnect(id);
|
||||
}
|
||||
}
|
||||
this.window_signal_ids = new Map();
|
||||
}
|
||||
|
||||
/// Callback when a new window is added
|
||||
on_window_actor_added(container, meta_window_actor) {
|
||||
this.window_signal_ids.set(meta_window_actor, [
|
||||
meta_window_actor.connect('notify::allocation',
|
||||
_ => this.update_visibility()
|
||||
),
|
||||
meta_window_actor.connect('notify::visible',
|
||||
_ => this.update_visibility()
|
||||
)
|
||||
]);
|
||||
this.update_visibility();
|
||||
}
|
||||
|
||||
/// Callback when a window is removed
|
||||
on_window_actor_removed(container, meta_window_actor) {
|
||||
for (const signalId of this.window_signal_ids.get(meta_window_actor)) {
|
||||
meta_window_actor.disconnect(signalId);
|
||||
}
|
||||
this.window_signal_ids.delete(meta_window_actor);
|
||||
this.update_visibility();
|
||||
}
|
||||
|
||||
/// Update the visibility of the blur effect
|
||||
update_visibility() {
|
||||
if (
|
||||
Main.panel.has_style_pseudo_class('overview')
|
||||
|| !Main.sessionMode.hasWindows
|
||||
) {
|
||||
this.actors_list.forEach(
|
||||
actors => this.set_should_override_panel(actors, true)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Main.layoutManager.primaryMonitor)
|
||||
return;
|
||||
|
||||
// get all the windows in the active workspace that are visible
|
||||
const workspace = global.workspace_manager.get_active_workspace();
|
||||
const windows = workspace.list_windows().filter(meta_window =>
|
||||
meta_window.showing_on_its_workspace()
|
||||
&& !meta_window.is_hidden()
|
||||
&& meta_window.get_window_type() !== Meta.WindowType.DESKTOP
|
||||
// exclude Desktop Icons NG
|
||||
&& meta_window.get_gtk_application_id() !== "com.rastersoft.ding"
|
||||
);
|
||||
|
||||
// check if at least one window is near enough to each panel and act
|
||||
// accordingly
|
||||
const scale = St.ThemeContext.get_for_stage(global.stage).scale_factor;
|
||||
this.actors_list
|
||||
// do not apply for dtp panels, as it would only cause bugs and it
|
||||
// can be done from its preferences anyway
|
||||
.filter(actors => !actors.is_dtp_panel)
|
||||
.forEach(actors => {
|
||||
let panel = actors.widgets.panel;
|
||||
let panel_top = panel.get_transformed_position()[1];
|
||||
let panel_bottom = panel_top + panel.get_height();
|
||||
|
||||
// check if at least a window is near enough the panel
|
||||
let window_overlap_panel = false;
|
||||
windows.forEach(meta_window => {
|
||||
let window_monitor_i = meta_window.get_monitor();
|
||||
let same_monitor = actors.monitor.index == window_monitor_i;
|
||||
|
||||
let window_vertical_pos = meta_window.get_frame_rect().y;
|
||||
|
||||
// if so, and if in the same monitor, then it overlaps
|
||||
if (same_monitor
|
||||
&&
|
||||
window_vertical_pos < panel_bottom + 5 * scale
|
||||
)
|
||||
window_overlap_panel = true;
|
||||
});
|
||||
|
||||
// if no window overlaps, then the panel is transparent
|
||||
this.set_should_override_panel(
|
||||
actors, !window_overlap_panel
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Choose wether or not the panel background should be overriden, in
|
||||
/// respect to its argument and the `override-background` setting.
|
||||
set_should_override_panel(actors, should_override) {
|
||||
let panel = actors.widgets.panel;
|
||||
|
||||
PANEL_STYLES.forEach(style => panel.remove_style_class_name(style));
|
||||
|
||||
if (
|
||||
this.settings.panel.OVERRIDE_BACKGROUND
|
||||
&&
|
||||
should_override
|
||||
)
|
||||
panel.add_style_class_name(
|
||||
PANEL_STYLES[this.settings.panel.STYLE_PANEL]
|
||||
);
|
||||
}
|
||||
|
||||
/// Fixes a bug where the blur is washed away when changing the sigma, or
|
||||
/// enabling/disabling other effects.
|
||||
invalidate_blur(actors) {
|
||||
if (this.settings.panel.STATIC_BLUR && actors.widgets.background)
|
||||
actors.widgets.background.get_content().invalidate();
|
||||
}
|
||||
|
||||
invalidate_all_blur() {
|
||||
this.actors_list.forEach(actors => this.invalidate_blur(actors));
|
||||
}
|
||||
|
||||
set_sigma(s) {
|
||||
this.actors_list.forEach(actors => {
|
||||
actors.effects.blur.sigma = s * actors.effects.blur.scale;
|
||||
this.invalidate_blur(actors);
|
||||
});
|
||||
}
|
||||
|
||||
set_brightness(b) {
|
||||
this.actors_list.forEach(actors => {
|
||||
actors.effects.blur.brightness = b;
|
||||
});
|
||||
}
|
||||
|
||||
set_color(c) {
|
||||
this.actors_list.forEach(actors => {
|
||||
actors.effects.color.color = c;
|
||||
});
|
||||
}
|
||||
|
||||
set_noise_amount(n) {
|
||||
this.actors_list.forEach(actors => {
|
||||
actors.effects.noise.noise = n;
|
||||
});
|
||||
}
|
||||
|
||||
set_noise_lightness(l) {
|
||||
this.actors_list.forEach(actors => {
|
||||
actors.effects.noise.lightness = l;
|
||||
});
|
||||
}
|
||||
|
||||
show() {
|
||||
this.actors_list.forEach(actors => {
|
||||
actors.widgets.background_parent.show();
|
||||
});
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.actors_list.forEach(actors => {
|
||||
actors.widgets.background_parent.hide();
|
||||
});
|
||||
}
|
||||
|
||||
// destroy every blurred background left, necessary after sleep
|
||||
destroy_blur_effects() {
|
||||
Main.panel?.get_parent()?.get_children().forEach(
|
||||
child => {
|
||||
if (child.name === 'topbar-blurred-background-parent') {
|
||||
child.get_children().forEach(meta_background_actor => {
|
||||
meta_background_actor.get_effects().forEach(effect => {
|
||||
this.effects_manager.remove(effect);
|
||||
});
|
||||
});
|
||||
child.destroy_all_children();
|
||||
child.destroy();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
disable() {
|
||||
this._log("removing blur from top panel");
|
||||
|
||||
this.disconnect_from_windows_and_overview();
|
||||
|
||||
this.actors_list.forEach(actors => {
|
||||
this.set_should_override_panel(actors, false);
|
||||
this.effects_manager.remove(actors.effects.noise);
|
||||
this.effects_manager.remove(actors.effects.color);
|
||||
this.effects_manager.remove(actors.effects.blur);
|
||||
try {
|
||||
actors.widgets.panel_box.remove_child(
|
||||
actors.widgets.background_parent
|
||||
);
|
||||
} catch (e) { }
|
||||
actors.widgets.background_parent?.destroy();
|
||||
});
|
||||
|
||||
this.destroy_blur_effects();
|
||||
|
||||
this.actors_list = [];
|
||||
|
||||
this.connections.disconnect_all();
|
||||
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
_log(str) {
|
||||
if (this.settings.DEBUG)
|
||||
console.log(`[Blur my Shell > panel] ${str}`);
|
||||
}
|
||||
|
||||
_warn(str) {
|
||||
console.warn(`[Blur my Shell > panel] ${str}`);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,173 @@
|
|||
import Shell from 'gi://Shell';
|
||||
import Meta from 'gi://Meta';
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
|
||||
|
||||
export const ScreenshotBlur = class ScreenshotBlur {
|
||||
constructor(connections, settings, effects_manager) {
|
||||
this.connections = connections;
|
||||
this.effects = [];
|
||||
this.settings = settings;
|
||||
this.effects_manager = effects_manager;
|
||||
}
|
||||
|
||||
enable() {
|
||||
this._log("blurring screenshot's window selector");
|
||||
|
||||
// connect to every background change (even without changing image)
|
||||
// FIXME this signal is fired very often, so we should find another one
|
||||
// fired only when necessary (but that still catches all cases)
|
||||
this.connections.connect(
|
||||
Main.layoutManager._backgroundGroup,
|
||||
'notify',
|
||||
_ => {
|
||||
this._log("updated background for screenshot's window selector");
|
||||
this.update_backgrounds();
|
||||
}
|
||||
);
|
||||
|
||||
// connect to monitors change
|
||||
this.connections.connect(
|
||||
Main.layoutManager,
|
||||
'monitors-changed',
|
||||
_ => {
|
||||
if (Main.screenShield && !Main.screenShield.locked) {
|
||||
this._log("changed monitors for screenshot's window selector");
|
||||
this.update_backgrounds();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// update backgrounds when the component is enabled
|
||||
this.update_backgrounds();
|
||||
}
|
||||
|
||||
update_backgrounds() {
|
||||
// remove every old background
|
||||
this.remove();
|
||||
|
||||
// add new backgrounds
|
||||
for (let i = 0; i < Main.screenshotUI._windowSelectors.length; i++) {
|
||||
const actor = Main.screenshotUI._windowSelectors[i];
|
||||
const monitor = Main.layoutManager.monitors[i];
|
||||
|
||||
if (!monitor)
|
||||
continue;
|
||||
|
||||
const bg_actor = this.create_background_actor(monitor);
|
||||
actor.insert_child_at_index(bg_actor, 0);
|
||||
actor._blur_actor = bg_actor;
|
||||
}
|
||||
}
|
||||
|
||||
create_background_actor(monitor) {
|
||||
let bg_actor = new Meta.BackgroundActor({
|
||||
meta_display: global.display,
|
||||
monitor: monitor.index
|
||||
});
|
||||
let background = Main.layoutManager._backgroundGroup.get_child_at_index(
|
||||
Main.layoutManager.monitors.length - monitor.index - 1
|
||||
);
|
||||
|
||||
if (!background) {
|
||||
this._warn("could not get background for screenshot's window selector");
|
||||
return bg_actor;
|
||||
}
|
||||
|
||||
bg_actor.content.set({
|
||||
background: background.get_content().background
|
||||
});
|
||||
|
||||
let blur_effect = new Shell.BlurEffect({
|
||||
brightness: this.settings.screenshot.CUSTOMIZE
|
||||
? this.settings.screenshot.BRIGHTNESS
|
||||
: this.settings.BRIGHTNESS,
|
||||
sigma: this.settings.screenshot.CUSTOMIZE
|
||||
? this.settings.screenshot.SIGMA
|
||||
: this.settings.SIGMA
|
||||
* monitor.geometry_scale,
|
||||
mode: Shell.BlurMode.ACTOR
|
||||
});
|
||||
|
||||
// store the scale in the effect in order to retrieve it in set_sigma
|
||||
blur_effect.scale = monitor.geometry_scale;
|
||||
|
||||
let color_effect = this.effects_manager.new_color_effect({
|
||||
color: this.settings.screenshot.CUSTOMIZE
|
||||
? this.settings.screenshot.COLOR
|
||||
: this.settings.COLOR
|
||||
}, this.settings);
|
||||
|
||||
let noise_effect = this.effects_manager.new_noise_effect({
|
||||
noise: this.settings.screenshot.CUSTOMIZE
|
||||
? this.settings.screenshot.NOISE_AMOUNT
|
||||
: this.settings.NOISE_AMOUNT,
|
||||
lightness: this.settings.screenshot.CUSTOMIZE
|
||||
? this.settings.screenshot.NOISE_LIGHTNESS
|
||||
: this.settings.NOISE_LIGHTNESS
|
||||
}, this.settings);
|
||||
|
||||
bg_actor.add_effect(color_effect);
|
||||
bg_actor.add_effect(noise_effect);
|
||||
bg_actor.add_effect(blur_effect);
|
||||
this.effects.push({ blur_effect, color_effect, noise_effect });
|
||||
|
||||
return bg_actor;
|
||||
}
|
||||
|
||||
set_sigma(s) {
|
||||
this.effects.forEach(effect => {
|
||||
effect.blur_effect.sigma = s * effect.blur_effect;
|
||||
});
|
||||
}
|
||||
|
||||
set_brightness(b) {
|
||||
this.effects.forEach(effect => {
|
||||
effect.blur_effect.brightness = b;
|
||||
});
|
||||
}
|
||||
|
||||
set_color(c) {
|
||||
this.effects.forEach(effect => {
|
||||
effect.color_effect.color = c;
|
||||
});
|
||||
}
|
||||
|
||||
set_noise_amount(n) {
|
||||
this.effects.forEach(effect => {
|
||||
effect.noise_effect.noise = n;
|
||||
});
|
||||
}
|
||||
|
||||
set_noise_lightness(l) {
|
||||
this.effects.forEach(effect => {
|
||||
effect.noise_effect.lightness = l;
|
||||
});
|
||||
}
|
||||
|
||||
remove() {
|
||||
Main.screenshotUI._windowSelectors.forEach(actor => {
|
||||
if (actor._blur_actor) {
|
||||
actor.remove_child(actor._blur_actor);
|
||||
actor._blur_actor.destroy();
|
||||
}
|
||||
});
|
||||
this.effects = [];
|
||||
}
|
||||
|
||||
disable() {
|
||||
this._log("removing blur from screenshot's window selector");
|
||||
|
||||
this.remove();
|
||||
this.connections.disconnect_all();
|
||||
}
|
||||
|
||||
_log(str) {
|
||||
if (this.settings.DEBUG)
|
||||
console.log(`[Blur my Shell > screenshot] ${str}`);
|
||||
}
|
||||
|
||||
_warn(str) {
|
||||
console.warn(`[Blur my Shell > screenshot] ${str}`);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,162 @@
|
|||
import Shell from 'gi://Shell';
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
|
||||
import { PaintSignals } from '../effects/paint_signals.js';
|
||||
|
||||
|
||||
export const WindowListBlur = class WindowListBlur {
|
||||
constructor(connections, settings, _) {
|
||||
this.connections = connections;
|
||||
this.settings = settings;
|
||||
this.paint_signals = new PaintSignals(connections);
|
||||
this.effects = [];
|
||||
}
|
||||
|
||||
enable() {
|
||||
this._log("blurring window list");
|
||||
|
||||
// blur if window-list is found
|
||||
Main.layoutManager.uiGroup.get_children().forEach(
|
||||
child => this.try_blur(child)
|
||||
);
|
||||
|
||||
// listen to new actors in `Main.layoutManager.uiGroup` and blur it if
|
||||
// if is window-list
|
||||
this.connections.connect(
|
||||
Main.layoutManager.uiGroup,
|
||||
'actor-added',
|
||||
(_, child) => this.try_blur(child)
|
||||
);
|
||||
|
||||
// connect to overview
|
||||
this.connections.connect(Main.overview, 'showing', _ => {
|
||||
this.hide();
|
||||
});
|
||||
this.connections.connect(Main.overview, 'hidden', _ => {
|
||||
this.show();
|
||||
});
|
||||
}
|
||||
|
||||
try_blur(child) {
|
||||
if (
|
||||
child.constructor.name === "WindowList" &&
|
||||
child.style !== "background:transparent;"
|
||||
) {
|
||||
this._log("found window list to blur");
|
||||
|
||||
let blur_effect = new Shell.BlurEffect({
|
||||
name: 'window-list-blur',
|
||||
sigma: this.settings.window_list.CUSTOMIZE
|
||||
? this.settings.window_list.SIGMA
|
||||
: this.settings.SIGMA,
|
||||
brightness: this.settings.window_list.CUSTOMIZE
|
||||
? this.settings.window_list.BRIGHTNESS
|
||||
: this.settings.BRIGHTNESS,
|
||||
mode: Shell.BlurMode.BACKGROUND
|
||||
});
|
||||
|
||||
child.set_style("background:transparent;");
|
||||
child.add_effect(blur_effect);
|
||||
this.effects.push({ blur_effect });
|
||||
|
||||
child._windowList.get_children().forEach(
|
||||
window => this.blur_window_button(window)
|
||||
);
|
||||
|
||||
this.connections.connect(
|
||||
child._windowList,
|
||||
'actor-added',
|
||||
(_, window) => this.blur_window_button(window)
|
||||
);
|
||||
|
||||
|
||||
// HACK
|
||||
//
|
||||
//`Shell.BlurEffect` does not repaint when shadows are under it. [1]
|
||||
//
|
||||
// This does not entirely fix this bug (shadows caused by windows
|
||||
// still cause artifacts), but it prevents the shadows of the panel
|
||||
// buttons to cause artifacts on the panel itself
|
||||
//
|
||||
// [1]: https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/2857
|
||||
|
||||
if (this.settings.HACKS_LEVEL === 1) {
|
||||
this._log("window list hack level 1");
|
||||
|
||||
this.paint_signals.connect(child, blur_effect);
|
||||
|
||||
} else if (this.settings.HACKS_LEVEL === 2) {
|
||||
this._log("window list hack level 2");
|
||||
|
||||
this.paint_signals.connect(child, blur_effect);
|
||||
} else {
|
||||
this.paint_signals.disconnect_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
blur_window_button(window) {
|
||||
window.get_child_at_index(0).set_style(
|
||||
"box-shadow:none; background-color:rgba(0,0,0,0.2); border-radius:5px;"
|
||||
);
|
||||
}
|
||||
|
||||
try_remove_blur(child) {
|
||||
if (
|
||||
child.constructor.name === "WindowList" &&
|
||||
child.style === "background:transparent;"
|
||||
) {
|
||||
child.style = null;
|
||||
child.remove_effect_by_name('window-list-blur');
|
||||
|
||||
child._windowList.get_children().forEach(
|
||||
child => child.get_child_at_index(0).set_style(null)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
set_sigma(s) {
|
||||
this.effects.forEach(effect => {
|
||||
effect.blur_effect.sigma = s;
|
||||
});
|
||||
}
|
||||
|
||||
set_brightness(b) {
|
||||
this.effects.forEach(effect => {
|
||||
effect.blur_effect.brightness = b;
|
||||
});
|
||||
}
|
||||
|
||||
// not implemented for dynamic blur
|
||||
set_color(c) { }
|
||||
set_noise_amount(n) { }
|
||||
set_noise_lightness(l) { }
|
||||
|
||||
hide() {
|
||||
this.set_sigma(0);
|
||||
}
|
||||
|
||||
show() {
|
||||
this.set_sigma(
|
||||
this.settings.window_list.CUSTOMIZE
|
||||
? this.settings.window_list.SIGMA
|
||||
: this.settings.SIGMA
|
||||
);
|
||||
}
|
||||
|
||||
disable() {
|
||||
this._log("removing blur from window list");
|
||||
|
||||
Main.layoutManager.uiGroup.get_children().forEach(
|
||||
child => this.try_remove_blur(child)
|
||||
);
|
||||
|
||||
this.effects = [];
|
||||
this.connections.disconnect_all();
|
||||
}
|
||||
|
||||
_log(str) {
|
||||
if (this.settings.DEBUG)
|
||||
console.log(`[Blur my Shell > window list] ${str}`);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,101 @@
|
|||
import GObject from 'gi://GObject';
|
||||
|
||||
/// An object to easily manage signals.
|
||||
export const Connections = class Connections {
|
||||
constructor() {
|
||||
this.buffer = [];
|
||||
}
|
||||
|
||||
/// Adds a connection.
|
||||
///
|
||||
/// Takes as arguments:
|
||||
/// - an actor, which fires the signal
|
||||
/// - signal(s) (string or array of strings), which are watched for
|
||||
/// - a callback, which is called when the signal is fired
|
||||
connect(actor, signals, handler) {
|
||||
if (signals instanceof Array) {
|
||||
signals.forEach(signal => {
|
||||
let id = actor.connect(signal, handler);
|
||||
this.process_connection(actor, id);
|
||||
});
|
||||
} else {
|
||||
let id = actor.connect(signals, handler);
|
||||
this.process_connection(actor, id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Process the given actor and id.
|
||||
///
|
||||
/// This makes sure that the signal is disconnected when the actor is
|
||||
/// destroyed, and that the signal can be managed through other Connections
|
||||
/// methods.
|
||||
process_connection(actor, id) {
|
||||
let infos = {
|
||||
actor: actor,
|
||||
id: id
|
||||
};
|
||||
|
||||
// remove the signal when the actor is destroyed
|
||||
if (
|
||||
actor.connect &&
|
||||
(
|
||||
!(actor instanceof GObject.Object) ||
|
||||
GObject.signal_lookup('destroy', actor)
|
||||
)
|
||||
) {
|
||||
let destroy_id = actor.connect('destroy', () => {
|
||||
actor.disconnect(id);
|
||||
actor.disconnect(destroy_id);
|
||||
|
||||
let index = this.buffer.indexOf(infos);
|
||||
if (index >= 0) {
|
||||
this.buffer.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.buffer.push(infos);
|
||||
}
|
||||
|
||||
/// Disconnects every connection found for an actor.
|
||||
disconnect_all_for(actor) {
|
||||
// get every connection stored for the actor
|
||||
let actor_connections = this.buffer.filter(
|
||||
infos => infos.actor === actor
|
||||
);
|
||||
|
||||
// remove each of them
|
||||
actor_connections.forEach((connection) => {
|
||||
// disconnect
|
||||
try {
|
||||
connection.actor.disconnect(connection.id);
|
||||
} catch (e) {
|
||||
this._warn(`error removing connection: ${e}; continuing`);
|
||||
}
|
||||
|
||||
// remove from buffer
|
||||
let index = this.buffer.indexOf(connection);
|
||||
this.buffer.splice(index, 1);
|
||||
});
|
||||
}
|
||||
|
||||
/// Disconnect every connection for each actor.
|
||||
disconnect_all() {
|
||||
this.buffer.forEach((connection) => {
|
||||
// disconnect
|
||||
try {
|
||||
connection.actor.disconnect(connection.id);
|
||||
} catch (e) {
|
||||
this._warn(`error removing connection: ${e}; continuing`);
|
||||
}
|
||||
});
|
||||
|
||||
// reset buffer
|
||||
this.buffer = [];
|
||||
}
|
||||
|
||||
_warn(str) {
|
||||
console.warn(`[Blur my Shell > connections] ${str}`);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,89 @@
|
|||
import { ColorEffect } from '../effects/color_effect.js';
|
||||
import { NoiseEffect } from '../effects/noise_effect.js';
|
||||
|
||||
|
||||
/// An object to manage effects (by not destroying them all the time)
|
||||
export const EffectsManager = class EffectsManager {
|
||||
constructor(connections) {
|
||||
this.connections = connections;
|
||||
this.used = [];
|
||||
this.color_effects = [];
|
||||
this.noise_effects = [];
|
||||
}
|
||||
|
||||
connect_to_destroy(effect) {
|
||||
effect.old_actor = effect.get_actor();
|
||||
if (effect.old_actor)
|
||||
effect.old_actor_id = effect.old_actor.connect('destroy', _ => {
|
||||
this.remove(effect);
|
||||
});
|
||||
|
||||
this.connections.connect(effect, 'notify::actor', _ => {
|
||||
let actor = effect.get_actor();
|
||||
|
||||
if (effect.old_actor && actor != effect.old_actor)
|
||||
effect.old_actor.disconnect(effect.old_actor_id);
|
||||
|
||||
if (actor) {
|
||||
effect.old_actor_id = actor.connect('destroy', _ => {
|
||||
this.remove(effect);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
new_color_effect(params, settings) {
|
||||
let effect;
|
||||
if (this.color_effects.length > 0) {
|
||||
effect = this.color_effects.splice(0, 1)[0];
|
||||
effect.set(params);
|
||||
} else
|
||||
effect = new ColorEffect(params, settings);
|
||||
|
||||
this.used.push(effect);
|
||||
this.connect_to_destroy(effect);
|
||||
return effect;
|
||||
}
|
||||
|
||||
new_noise_effect(params, settings) {
|
||||
let effect;
|
||||
if (this.noise_effects.length > 0) {
|
||||
effect = this.noise_effects.splice(0, 1)[0];
|
||||
effect.set(params);
|
||||
} else
|
||||
effect = new NoiseEffect(params, settings);
|
||||
|
||||
this.used.push(effect);
|
||||
this.connect_to_destroy(effect);
|
||||
return effect;
|
||||
}
|
||||
|
||||
remove(effect) {
|
||||
effect.get_actor()?.remove_effect(effect);
|
||||
if (effect.old_actor)
|
||||
effect.old_actor.disconnect(effect.old_actor_id);
|
||||
delete effect.old_actor;
|
||||
delete effect.old_actor_id;
|
||||
|
||||
let index = this.used.indexOf(effect);
|
||||
if (index >= 0) {
|
||||
this.used.splice(index, 1);
|
||||
|
||||
if (effect instanceof ColorEffect)
|
||||
this.color_effects.push(effect);
|
||||
else if (effect instanceof NoiseEffect)
|
||||
this.noise_effects.push(effect);
|
||||
}
|
||||
}
|
||||
|
||||
destroy_all() {
|
||||
this.used.forEach(effect => { this.remove(effect); });
|
||||
[
|
||||
this.used,
|
||||
this.color_effects,
|
||||
this.noise_effects
|
||||
].forEach(array => {
|
||||
array.splice(0, array.length);
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,131 @@
|
|||
import { Type } from './settings.js';
|
||||
|
||||
// This lists the preferences keys
|
||||
export const Keys = [
|
||||
{
|
||||
component: "general", schemas: [
|
||||
{ type: Type.I, name: "sigma" },
|
||||
{ type: Type.D, name: "brightness" },
|
||||
{ type: Type.C, name: "color" },
|
||||
{ type: Type.D, name: "noise-amount" },
|
||||
{ type: Type.D, name: "noise-lightness" },
|
||||
{ type: Type.B, name: "color-and-noise" },
|
||||
{ type: Type.I, name: "hacks-level" },
|
||||
{ type: Type.B, name: "debug" },
|
||||
]
|
||||
},
|
||||
{
|
||||
component: "overview", schemas: [
|
||||
{ type: Type.B, name: "blur" },
|
||||
{ type: Type.B, name: "customize" },
|
||||
{ type: Type.I, name: "sigma" },
|
||||
{ type: Type.D, name: "brightness" },
|
||||
{ type: Type.C, name: "color" },
|
||||
{ type: Type.D, name: "noise-amount" },
|
||||
{ type: Type.D, name: "noise-lightness" },
|
||||
{ type: Type.I, name: "style-components" },
|
||||
]
|
||||
},
|
||||
{
|
||||
component: "appfolder", schemas: [
|
||||
{ type: Type.B, name: "blur" },
|
||||
{ type: Type.B, name: "customize" },
|
||||
{ type: Type.I, name: "sigma" },
|
||||
{ type: Type.D, name: "brightness" },
|
||||
{ type: Type.C, name: "color" },
|
||||
{ type: Type.D, name: "noise-amount" },
|
||||
{ type: Type.D, name: "noise-lightness" },
|
||||
{ type: Type.I, name: "style-dialogs" },
|
||||
]
|
||||
},
|
||||
{
|
||||
component: "panel", schemas: [
|
||||
{ type: Type.B, name: "blur" },
|
||||
{ type: Type.B, name: "customize" },
|
||||
{ type: Type.I, name: "sigma" },
|
||||
{ type: Type.D, name: "brightness" },
|
||||
{ type: Type.C, name: "color" },
|
||||
{ type: Type.D, name: "noise-amount" },
|
||||
{ type: Type.D, name: "noise-lightness" },
|
||||
{ type: Type.B, name: "static-blur" },
|
||||
{ type: Type.B, name: "unblur-in-overview" },
|
||||
{ type: Type.B, name: "override-background" },
|
||||
{ type: Type.I, name: "style-panel" },
|
||||
{ type: Type.B, name: "override-background-dynamically" },
|
||||
]
|
||||
},
|
||||
{
|
||||
component: "dash-to-dock", schemas: [
|
||||
{ type: Type.B, name: "blur" },
|
||||
{ type: Type.B, name: "customize" },
|
||||
{ type: Type.I, name: "sigma" },
|
||||
{ type: Type.D, name: "brightness" },
|
||||
{ type: Type.C, name: "color" },
|
||||
{ type: Type.D, name: "noise-amount" },
|
||||
{ type: Type.D, name: "noise-lightness" },
|
||||
{ type: Type.B, name: "static-blur" },
|
||||
{ type: Type.B, name: "unblur-in-overview" },
|
||||
{ type: Type.B, name: "override-background" },
|
||||
{ type: Type.I, name: "style-dash-to-dock" },
|
||||
]
|
||||
},
|
||||
{
|
||||
component: "applications", schemas: [
|
||||
{ type: Type.B, name: "blur" },
|
||||
{ type: Type.B, name: "customize" },
|
||||
{ type: Type.I, name: "sigma" },
|
||||
{ type: Type.D, name: "brightness" },
|
||||
{ type: Type.C, name: "color" },
|
||||
{ type: Type.D, name: "noise-amount" },
|
||||
{ type: Type.D, name: "noise-lightness" },
|
||||
{ type: Type.I, name: "opacity" },
|
||||
{ type: Type.B, name: "blur-on-overview" },
|
||||
{ type: Type.B, name: "enable-all" },
|
||||
{ type: Type.AS, name: "whitelist" },
|
||||
{ type: Type.AS, name: "blacklist" },
|
||||
]
|
||||
},
|
||||
{
|
||||
component: "lockscreen", schemas: [
|
||||
{ type: Type.B, name: "blur" },
|
||||
{ type: Type.B, name: "customize" },
|
||||
{ type: Type.I, name: "sigma" },
|
||||
{ type: Type.D, name: "brightness" },
|
||||
{ type: Type.C, name: "color" },
|
||||
{ type: Type.D, name: "noise-amount" },
|
||||
{ type: Type.D, name: "noise-lightness" },
|
||||
]
|
||||
},
|
||||
{
|
||||
component: "window-list", schemas: [
|
||||
{ type: Type.B, name: "blur" },
|
||||
{ type: Type.B, name: "customize" },
|
||||
{ type: Type.I, name: "sigma" },
|
||||
{ type: Type.D, name: "brightness" },
|
||||
{ type: Type.C, name: "color" },
|
||||
{ type: Type.D, name: "noise-amount" },
|
||||
{ type: Type.D, name: "noise-lightness" },
|
||||
]
|
||||
},
|
||||
{
|
||||
component: "screenshot", schemas: [
|
||||
{ type: Type.B, name: "blur" },
|
||||
{ type: Type.B, name: "customize" },
|
||||
{ type: Type.I, name: "sigma" },
|
||||
{ type: Type.D, name: "brightness" },
|
||||
{ type: Type.C, name: "color" },
|
||||
{ type: Type.D, name: "noise-amount" },
|
||||
{ type: Type.D, name: "noise-lightness" },
|
||||
]
|
||||
},
|
||||
{
|
||||
component: "hidetopbar", schemas: [
|
||||
{ type: Type.B, name: "compatibility" },
|
||||
]
|
||||
},
|
||||
{
|
||||
component: "dash-to-panel", schemas: [
|
||||
{ type: Type.B, name: "blur-original-panel" },
|
||||
]
|
||||
},
|
||||
];
|
|
@ -0,0 +1,182 @@
|
|||
import GLib from 'gi://GLib';
|
||||
|
||||
const Signals = imports.signals;
|
||||
|
||||
/// An enum non-extensively describing the type of gsettings key.
|
||||
export const Type = {
|
||||
B: 'Boolean',
|
||||
I: 'Integer',
|
||||
D: 'Double',
|
||||
S: 'String',
|
||||
C: 'Color',
|
||||
AS: 'StringArray'
|
||||
};
|
||||
|
||||
/// An object to get and manage the gsettings preferences.
|
||||
///
|
||||
/// Should be initialized with an array of keys, for example:
|
||||
///
|
||||
/// let settings = new Settings([
|
||||
/// { type: Type.I, name: "panel-corner-radius" },
|
||||
/// { type: Type.B, name: "debug" }
|
||||
/// ]);
|
||||
///
|
||||
/// Each {type, name} object represents a gsettings key, which must be created
|
||||
/// in the gschemas.xml file of the extension.
|
||||
export const Settings = class Settings {
|
||||
constructor(keys, settings) {
|
||||
this.settings = settings;
|
||||
this.keys = keys;
|
||||
|
||||
this.keys.forEach(bundle => {
|
||||
let component = this;
|
||||
let component_settings = settings;
|
||||
if (bundle.component !== "general") {
|
||||
let bundle_component = bundle.component.replaceAll('-', '_');
|
||||
this[bundle_component] = {
|
||||
settings: this.settings.get_child(bundle.component)
|
||||
};
|
||||
component = this[bundle_component];
|
||||
component_settings = settings.get_child(bundle.component);
|
||||
}
|
||||
|
||||
|
||||
bundle.schemas.forEach(key => {
|
||||
let property_name = this.get_property_name(key.name);
|
||||
|
||||
switch (key.type) {
|
||||
case Type.B:
|
||||
Object.defineProperty(component, property_name, {
|
||||
get() {
|
||||
return component_settings.get_boolean(key.name);
|
||||
},
|
||||
set(v) {
|
||||
component_settings.set_boolean(key.name, v);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case Type.I:
|
||||
Object.defineProperty(component, property_name, {
|
||||
get() {
|
||||
return component_settings.get_int(key.name);
|
||||
},
|
||||
set(v) {
|
||||
component_settings.set_int(key.name, v);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case Type.D:
|
||||
Object.defineProperty(component, property_name, {
|
||||
get() {
|
||||
return component_settings.get_double(key.name);
|
||||
},
|
||||
set(v) {
|
||||
component_settings.set_double(key.name, v);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case Type.S:
|
||||
Object.defineProperty(component, property_name, {
|
||||
get() {
|
||||
return component_settings.get_string(key.name);
|
||||
},
|
||||
set(v) {
|
||||
component_settings.set_string(key.name, v);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case Type.C:
|
||||
Object.defineProperty(component, property_name, {
|
||||
// returns the array [red, blue, green, alpha] with
|
||||
// values between 0 and 1
|
||||
get() {
|
||||
let val = component_settings.get_value(key.name);
|
||||
return val.deep_unpack();
|
||||
},
|
||||
// takes an array [red, blue, green, alpha] with
|
||||
// values between 0 and 1
|
||||
set(v) {
|
||||
let val = new GLib.Variant("(dddd)", v);
|
||||
component_settings.set_value(key.name, val);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case Type.AS:
|
||||
Object.defineProperty(component, property_name, {
|
||||
get() {
|
||||
let val = component_settings.get_value(key.name);
|
||||
return val.deep_unpack();
|
||||
},
|
||||
set(v) {
|
||||
let val = new GLib.Variant("as", v);
|
||||
component_settings.set_value(key.name, val);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
component[property_name + '_reset'] = function () {
|
||||
return component_settings.reset(key.name);
|
||||
};
|
||||
|
||||
component[property_name + '_changed'] = function (cb) {
|
||||
return component_settings.connect('changed::' + key.name, cb);
|
||||
};
|
||||
|
||||
component[property_name + '_disconnect'] = function () {
|
||||
return component_settings.disconnect.apply(
|
||||
component_settings, arguments
|
||||
);
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/// Reset the preferences.
|
||||
reset() {
|
||||
this.keys.forEach(bundle => {
|
||||
let component = this;
|
||||
if (bundle.component !== "general") {
|
||||
let bundle_component = bundle.component.replaceAll('-', '_');
|
||||
component = this[bundle_component];
|
||||
}
|
||||
|
||||
bundle.schemas.forEach(key => {
|
||||
let property_name = this.get_property_name(key.name);
|
||||
component[property_name + '_reset']();
|
||||
});
|
||||
});
|
||||
|
||||
this.emit('reset', true);
|
||||
}
|
||||
|
||||
/// From the gschema name, returns the name of the associated property on
|
||||
/// the Settings object.
|
||||
get_property_name(name) {
|
||||
return name.replaceAll('-', '_').toUpperCase();
|
||||
}
|
||||
|
||||
/// Remove all connections managed by the Settings object, i.e. created with
|
||||
/// `settings.PROPERTY_changed(callback)`.
|
||||
disconnect_all_settings() {
|
||||
this.keys.forEach(bundle => {
|
||||
let component = this;
|
||||
if (bundle.component !== "general") {
|
||||
let bundle_component = bundle.component.replaceAll('-', '_');
|
||||
component = this[bundle_component];
|
||||
}
|
||||
|
||||
bundle.schemas.forEach(key => {
|
||||
let property_name = this.get_property_name(key.name);
|
||||
component[property_name + '_disconnect']();
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Signals.addSignalMethods(Settings.prototype);
|
|
@ -0,0 +1,58 @@
|
|||
import Gio from 'gi://Gio';
|
||||
|
||||
const bus_name = 'org.gnome.Shell';
|
||||
const iface_name = 'dev.aunetx.BlurMyShell';
|
||||
const obj_path = '/dev/aunetx/BlurMyShell';
|
||||
|
||||
|
||||
/// Call pick() from the DBus service, it will open the Inspector from
|
||||
/// gnome-shell to pick an actor on stage.
|
||||
export function pick() {
|
||||
Gio.DBus.session.call(
|
||||
bus_name,
|
||||
obj_path,
|
||||
iface_name,
|
||||
'pick',
|
||||
null,
|
||||
null,
|
||||
Gio.DBusCallFlags.NO_AUTO_START,
|
||||
-1,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/// Connect to DBus 'picking' signal, which will be emitted when the inspector
|
||||
/// is picking a window.
|
||||
export function on_picking(cb) {
|
||||
const id = Gio.DBus.session.signal_subscribe(
|
||||
bus_name,
|
||||
iface_name,
|
||||
'picking',
|
||||
obj_path,
|
||||
null,
|
||||
Gio.DBusSignalFlags.NONE,
|
||||
_ => {
|
||||
cb();
|
||||
Gio.DBus.session.signal_unsubscribe(id);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// Connect to DBus 'picked' signal, which will be emitted when a window is
|
||||
/// picked.
|
||||
export function on_picked(cb) {
|
||||
const id = Gio.DBus.session.signal_subscribe(
|
||||
bus_name,
|
||||
iface_name,
|
||||
'picked',
|
||||
obj_path,
|
||||
null,
|
||||
Gio.DBusSignalFlags.NONE,
|
||||
(conn, sender, obj_path, iface, signal, params) => {
|
||||
const val = params.get_child_value(0);
|
||||
cb(val.get_string()[0]);
|
||||
Gio.DBus.session.signal_unsubscribe(id);
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<node>
|
||||
<interface name="dev.aunetx.BlurMyShell">
|
||||
<!-- This method is called in preferences to pick a window -->
|
||||
<method name="pick" />
|
||||
<!-- When window is picking, send a signal to preferences -->
|
||||
<signal name="picking"></signal>
|
||||
<!-- If window is picked, send a signal to preferences -->
|
||||
<signal name="picked">
|
||||
<arg name="window" type="s" />
|
||||
</signal>
|
||||
</interface>
|
||||
</node>
|
|
@ -0,0 +1,90 @@
|
|||
import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
import * as LookingGlass from 'resource:///org/gnome/shell/ui/lookingGlass.js';
|
||||
|
||||
|
||||
export const ApplicationsService = class ApplicationsService {
|
||||
constructor() {
|
||||
let decoder = new TextDecoder();
|
||||
let path = GLib.filename_from_uri(GLib.uri_resolve_relative(
|
||||
import.meta.url, 'iface.xml', GLib.UriFlags.NONE)
|
||||
)[0];
|
||||
let [, buffer] = GLib.file_get_contents(path);
|
||||
let iface = decoder.decode(buffer);
|
||||
GLib.free(buffer);
|
||||
|
||||
this.DBusImpl = Gio.DBusExportedObject.wrapJSObject(iface, this);
|
||||
}
|
||||
|
||||
/// Pick Window for Preferences Page, exported to DBus client.
|
||||
pick() {
|
||||
// emit `picking` signal to know we are listening
|
||||
const send_picking_signal = _ =>
|
||||
this.DBusImpl.emit_signal(
|
||||
'picking',
|
||||
null
|
||||
);
|
||||
|
||||
// emit `picked` signal to send wm_class
|
||||
const send_picked_signal = wm_class =>
|
||||
this.DBusImpl.emit_signal(
|
||||
'picked',
|
||||
new GLib.Variant('(s)', [wm_class])
|
||||
);
|
||||
|
||||
// notify the preferences that we are listening
|
||||
send_picking_signal();
|
||||
|
||||
// A very interesting way to pick a window:
|
||||
// 1. Open LookingGlass to mask all event handles of window
|
||||
// 2. Use inspector to pick window, thats is also lookingGlass do
|
||||
// 3. Close LookingGlass when done
|
||||
// It will restore event handles of window
|
||||
|
||||
// open then hide LookingGlass
|
||||
const looking_class = Main.createLookingGlass();
|
||||
looking_class.open();
|
||||
looking_class.hide();
|
||||
|
||||
// inspect window now
|
||||
const inspector = new LookingGlass.Inspector(Main.createLookingGlass());
|
||||
inspector.connect('target', (me, target, x, y) => {
|
||||
// remove border effect when window is picked.
|
||||
const effect_name = 'lookingGlass_RedBorderEffect';
|
||||
target
|
||||
.get_effects()
|
||||
.filter(e => e.toString().includes(effect_name))
|
||||
.forEach(e => target.remove_effect(e));
|
||||
|
||||
// get wm_class_instance property of window, then pass it to DBus
|
||||
// client
|
||||
const type_str = target.toString();
|
||||
|
||||
let actor = target;
|
||||
if (type_str.includes('MetaSurfaceActor'))
|
||||
actor = target.get_parent();
|
||||
|
||||
if (!actor.toString().includes('WindowActor'))
|
||||
return send_picked_signal('window-not-found');
|
||||
|
||||
send_picked_signal(
|
||||
actor.meta_window.get_wm_class() ?? 'window-not-found'
|
||||
);
|
||||
});
|
||||
|
||||
// close LookingGlass when we're done
|
||||
inspector.connect('closed', _ => looking_class.close());
|
||||
}
|
||||
|
||||
export() {
|
||||
this.DBusImpl.export(
|
||||
Gio.DBus.session,
|
||||
'/dev/aunetx/BlurMyShell'
|
||||
);
|
||||
};
|
||||
|
||||
unexport() {
|
||||
this.DBusImpl.unexport();
|
||||
}
|
||||
};
|