IndicatorsEntry updated, code cleanup
Andrew P.
9 years ago
1 | 1 | <!-- Generated with glade 3.18.3 --> |
2 | 2 | <interface> |
3 | 3 | <requires lib="gtk+" version="3.8"/> |
4 | <requires lib="" version="3.8"/> | |
4 | 5 | <requires lib="gtk_greeter_settings" version="1.0"/> |
5 | 6 | <object class="GtkMenu" id="greeter_default-user-image_menu"> |
6 | 7 | <property name="visible">True</property> |
26 | 27 | <column type="gchararray"/> |
27 | 28 | <!-- column-name tooltip --> |
28 | 29 | <column type="gchararray"/> |
29 | <!-- column-name editable --> | |
30 | <column type="gboolean"/> | |
31 | 30 | <!-- column-name has-state --> |
32 | 31 | <column type="gboolean"/> |
33 | 32 | <!-- column-name state --> |
34 | 33 | <column type="gboolean"/> |
34 | <!-- column-name options --> | |
35 | <column type="GObject"/> | |
36 | <!-- column-name icon --> | |
37 | <column type="gchararray"/> | |
38 | <!-- column-name markup --> | |
39 | <column type="gchararray"/> | |
35 | 40 | </columns> |
36 | <data> | |
37 | <row> | |
38 | <col id="0">~host</col> | |
39 | <col id="1" translatable="yes" context="option|greeter|indicators">Host name</col> | |
40 | <col id="2">False</col> | |
41 | <col id="3">True</col> | |
42 | <col id="4">False</col> | |
43 | </row> | |
44 | <row> | |
45 | <col id="0">~clock</col> | |
46 | <col id="1" translatable="yes" context="option|greeter|indicators">Clock</col> | |
47 | <col id="2">False</col> | |
48 | <col id="3">True</col> | |
49 | <col id="4">False</col> | |
50 | </row> | |
51 | <row> | |
52 | <col id="0">~layout</col> | |
53 | <col id="1" translatable="yes" context="option|greeter|indicators">Layout indicator</col> | |
54 | <col id="2">False</col> | |
55 | <col id="3">True</col> | |
56 | <col id="4">False</col> | |
57 | </row> | |
58 | <row> | |
59 | <col id="0">~session</col> | |
60 | <col id="1" translatable="yes" context="option|greeter|indicators">Sessions menu (xfce, unity, gnome etc.)</col> | |
61 | <col id="2">False</col> | |
62 | <col id="3">True</col> | |
63 | <col id="4">False</col> | |
64 | </row> | |
65 | <row> | |
66 | <col id="0">~language</col> | |
67 | <col id="1" translatable="yes" context="option|greeter|indicators">Languages menu</col> | |
68 | <col id="2">False</col> | |
69 | <col id="3">True</col> | |
70 | <col id="4">False</col> | |
71 | </row> | |
72 | <row> | |
73 | <col id="0">~a11y</col> | |
74 | <col id="1" translatable="yes" context="option|greeter|indicators">Accessibility menu</col> | |
75 | <col id="2">False</col> | |
76 | <col id="3">True</col> | |
77 | <col id="4">False</col> | |
78 | </row> | |
79 | <row> | |
80 | <col id="0">~power</col> | |
81 | <col id="1" translatable="yes" context="option|greeter|indicators">Power menu</col> | |
82 | <col id="2">False</col> | |
83 | <col id="3">True</col> | |
84 | <col id="4">False</col> | |
85 | </row> | |
86 | </data> | |
87 | 41 | </object> |
88 | 42 | <object class="GtkAdjustment" id="greeter_position_x_adjustment"> |
89 | 43 | <property name="upper">10000</property> |
121 | 75 | <property name="title" translatable="yes">LightDM GTK+ Greeter: settings</property> |
122 | 76 | <property name="window_position">center</property> |
123 | 77 | <property name="icon_name">lightdm-gtk-greeter-settings</property> |
124 | <property name="has_resize_grip">True</property> | |
125 | 78 | <signal name="destroy" handler="on_destroy" swapped="no"/> |
126 | 79 | <child> |
127 | 80 | <object class="GtkBox" id="box1"> |
367 | 320 | </packing> |
368 | 321 | </child> |
369 | 322 | <child> |
370 | <object class="GtkRadioButton" id="greeter_background_image_choice"> | |
371 | <property name="label" translatable="yes" context="option|greeter|background">Image</property> | |
372 | <property name="visible">True</property> | |
373 | <property name="can_focus">False</property> | |
374 | <property name="receives_default">False</property> | |
375 | <property name="halign">start</property> | |
376 | <property name="margin_left">24</property> | |
377 | <property name="xalign">0.5</property> | |
378 | <property name="active">True</property> | |
379 | <property name="draw_indicator">True</property> | |
380 | </object> | |
381 | <packing> | |
382 | <property name="left_attach">0</property> | |
383 | <property name="top_attach">5</property> | |
384 | </packing> | |
385 | </child> | |
386 | <child> | |
387 | 323 | <object class="GtkFileChooserButton" id="greeter_background_image_value"> |
388 | 324 | <property name="visible">True</property> |
389 | 325 | <property name="can_focus">False</property> |
392 | 328 | <packing> |
393 | 329 | <property name="left_attach">1</property> |
394 | 330 | <property name="top_attach">5</property> |
395 | </packing> | |
396 | </child> | |
397 | <child> | |
398 | <object class="GtkRadioButton" id="greeter_background_color_choice"> | |
399 | <property name="label" translatable="yes" context="option|greeter|background">Color</property> | |
400 | <property name="visible">True</property> | |
401 | <property name="can_focus">False</property> | |
402 | <property name="receives_default">False</property> | |
403 | <property name="halign">start</property> | |
404 | <property name="margin_left">24</property> | |
405 | <property name="xalign">0.5</property> | |
406 | <property name="active">True</property> | |
407 | <property name="draw_indicator">True</property> | |
408 | <property name="group">greeter_background_image_choice</property> | |
409 | </object> | |
410 | <packing> | |
411 | <property name="left_attach">0</property> | |
412 | <property name="top_attach">6</property> | |
413 | 331 | </packing> |
414 | 332 | </child> |
415 | 333 | <child> |
706 | 624 | <packing> |
707 | 625 | <property name="left_attach">0</property> |
708 | 626 | <property name="top_attach">9</property> |
627 | </packing> | |
628 | </child> | |
629 | <child> | |
630 | <object class="GtkRadioButton" id="greeter_background_color_choice"> | |
631 | <property name="label" translatable="yes" context="option|greeter|background">Color</property> | |
632 | <property name="visible">True</property> | |
633 | <property name="can_focus">False</property> | |
634 | <property name="receives_default">False</property> | |
635 | <property name="halign">start</property> | |
636 | <property name="margin_left">24</property> | |
637 | <property name="xalign">0.5</property> | |
638 | <property name="active">True</property> | |
639 | <property name="draw_indicator">True</property> | |
640 | <property name="group">greeter_background_image_choice</property> | |
641 | </object> | |
642 | <packing> | |
643 | <property name="left_attach">0</property> | |
644 | <property name="top_attach">6</property> | |
645 | </packing> | |
646 | </child> | |
647 | <child> | |
648 | <object class="GtkRadioButton" id="greeter_background_image_choice"> | |
649 | <property name="label" translatable="yes" context="option|greeter|background">Image</property> | |
650 | <property name="visible">True</property> | |
651 | <property name="can_focus">False</property> | |
652 | <property name="receives_default">False</property> | |
653 | <property name="halign">start</property> | |
654 | <property name="margin_left">24</property> | |
655 | <property name="xalign">0.5</property> | |
656 | <property name="active">True</property> | |
657 | <property name="draw_indicator">True</property> | |
658 | </object> | |
659 | <packing> | |
660 | <property name="left_attach">0</property> | |
661 | <property name="top_attach">5</property> | |
709 | 662 | </packing> |
710 | 663 | </child> |
711 | 664 | </object> |
809 | 762 | <property name="orientation">vertical</property> |
810 | 763 | <child> |
811 | 764 | <object class="GtkScrolledWindow" id="scrolledwindow1"> |
812 | <property name="height_request">160</property> | |
765 | <property name="height_request">170</property> | |
813 | 766 | <property name="visible">True</property> |
814 | 767 | <property name="can_focus">True</property> |
815 | 768 | <property name="hexpand">True</property> |
825 | 778 | <property name="headers_visible">False</property> |
826 | 779 | <property name="headers_clickable">False</property> |
827 | 780 | <property name="reorderable">True</property> |
828 | <property name="search_column">0</property> | |
781 | <property name="search_column">3</property> | |
829 | 782 | <property name="tooltip_column">1</property> |
830 | 783 | <child internal-child="selection"> |
831 | 784 | <object class="GtkTreeSelection" id="greeter_indicators_selection"> |
834 | 787 | </child> |
835 | 788 | <child> |
836 | 789 | <object class="GtkTreeViewColumn" id="greeter_indicators_state_column"> |
790 | <property name="visible">False</property> | |
837 | 791 | <property name="sizing">autosize</property> |
838 | 792 | <property name="title">column</property> |
839 | 793 | <property name="clickable">True</property> |
840 | 794 | <child> |
841 | 795 | <object class="GtkCellRendererToggle" id="greeter_indicators_state_renderer"/> |
842 | 796 | <attributes> |
843 | <attribute name="visible">3</attribute> | |
844 | <attribute name="active">4</attribute> | |
797 | <attribute name="visible">2</attribute> | |
798 | <attribute name="active">3</attribute> | |
845 | 799 | </attributes> |
846 | 800 | </child> |
847 | 801 | </object> |
852 | 806 | <property name="title">column</property> |
853 | 807 | <property name="expand">True</property> |
854 | 808 | <child> |
809 | <object class="GtkCellRendererPixbuf" id="greeter_indicators_image_renderer"/> | |
810 | <attributes> | |
811 | <attribute name="icon-name">5</attribute> | |
812 | </attributes> | |
813 | </child> | |
814 | <child> | |
855 | 815 | <object class="GtkCellRendererText" id="greeter_indicators_name_renderer"/> |
856 | 816 | <attributes> |
857 | <attribute name="editable">3</attribute> | |
858 | <attribute name="text">0</attribute> | |
817 | <attribute name="markup">6</attribute> | |
859 | 818 | </attributes> |
860 | 819 | </child> |
861 | 820 | </object> |
928 | 887 | <packing> |
929 | 888 | <property name="expand">False</property> |
930 | 889 | <property name="homogeneous">True</property> |
890 | </packing> | |
891 | </child> | |
892 | <child> | |
893 | <object class="GtkToolButton" id="greeter_indicators_tools"> | |
894 | <property name="visible">True</property> | |
895 | <property name="can_focus">False</property> | |
896 | <property name="label" translatable="yes" context="option|greeter|indicators">Templates</property> | |
897 | <property name="use_underline">True</property> | |
898 | <property name="icon_name">preferences-other</property> | |
899 | </object> | |
900 | <packing> | |
901 | <property name="expand">False</property> | |
902 | <property name="homogeneous">False</property> | |
931 | 903 | </packing> |
932 | 904 | </child> |
933 | 905 | <style> |
1646 | 1618 | </packing> |
1647 | 1619 | </child> |
1648 | 1620 | <child> |
1649 | <object class="GtkInfoBar" id="no_access_infobar"> | |
1621 | <object class="GtkInfoBar" id="infobar"> | |
1650 | 1622 | <property name="visible">True</property> |
1651 | 1623 | <property name="app_paintable">True</property> |
1652 | 1624 | <property name="can_focus">False</property> |
154 | 154 | </packing> |
155 | 155 | </child> |
156 | 156 | <child> |
157 | <object class="GtkEntry" id="name"> | |
157 | <object class="GtkEntry" id="name_entry"> | |
158 | 158 | <property name="visible">True</property> |
159 | 159 | <property name="can_focus">True</property> |
160 | 160 | <property name="invisible_char">●</property> |
198 | 198 | <packing> |
199 | 199 | <property name="left_attach">1</property> |
200 | 200 | <property name="top_attach">1</property> |
201 | </packing> | |
202 | </child> | |
203 | <child> | |
204 | <object class="GtkImage" id="preview"> | |
205 | <property name="visible">True</property> | |
206 | <property name="can_focus">False</property> | |
207 | <property name="halign">center</property> | |
208 | <property name="valign">start</property> | |
209 | <property name="stock">gtk-missing-image</property> | |
210 | <property name="pixel_size">32</property> | |
211 | <property name="icon_size">6</property> | |
212 | </object> | |
213 | <packing> | |
214 | <property name="left_attach">2</property> | |
215 | <property name="top_attach">0</property> | |
216 | <property name="height">2</property> | |
217 | 201 | </packing> |
218 | 202 | </child> |
219 | 203 | <child> |
327 | 311 | <property name="top_attach">2</property> |
328 | 312 | </packing> |
329 | 313 | </child> |
314 | <child> | |
315 | <object class="GtkImage" id="preview_image"> | |
316 | <property name="width_request">32</property> | |
317 | <property name="height_request">32</property> | |
318 | <property name="visible">True</property> | |
319 | <property name="can_focus">False</property> | |
320 | <property name="halign">center</property> | |
321 | <property name="valign">start</property> | |
322 | <property name="pixel_size">32</property> | |
323 | <property name="icon_size">6</property> | |
324 | </object> | |
325 | <packing> | |
326 | <property name="left_attach">2</property> | |
327 | <property name="top_attach">0</property> | |
328 | <property name="height">2</property> | |
329 | </packing> | |
330 | </child> | |
330 | 331 | </object> |
331 | 332 | <packing> |
332 | 333 | <property name="expand">False</property> |
0 | <?xml version="1.0" encoding="UTF-8"?> | |
1 | <!-- Generated with glade 3.18.3 --> | |
2 | <interface> | |
3 | <requires lib="gtk+" version="3.8"/> | |
4 | <requires lib="gtk_greeter_settings" version="1.0"/> | |
5 | <object class="GtkImage" id="image1"> | |
6 | <property name="visible">True</property> | |
7 | <property name="can_focus">False</property> | |
8 | <property name="xpad">2</property> | |
9 | <property name="icon_name">list-add</property> | |
10 | </object> | |
11 | <object class="GtkImage" id="image2"> | |
12 | <property name="visible">True</property> | |
13 | <property name="can_focus">False</property> | |
14 | <property name="xpad">2</property> | |
15 | <property name="icon_name">window-close</property> | |
16 | </object> | |
17 | <object class="GtkImage" id="image3"> | |
18 | <property name="visible">True</property> | |
19 | <property name="can_focus">False</property> | |
20 | <property name="xpad">2</property> | |
21 | <property name="icon_name">dialog-ok</property> | |
22 | </object> | |
23 | <object class="GtkFileFilter" id="library_file_filter"> | |
24 | <mime-types> | |
25 | <mime-type>application/x-sharedlib</mime-type> | |
26 | </mime-types> | |
27 | </object> | |
28 | <object class="GtkListStore" id="short_model"> | |
29 | <columns> | |
30 | <!-- column-name name --> | |
31 | <column type="gchararray"/> | |
32 | </columns> | |
33 | </object> | |
34 | <object class="IndicatorChooserDialog" id="indicator_chooser_dialog"> | |
35 | <property name="width_request">400</property> | |
36 | <property name="can_focus">False</property> | |
37 | <property name="title" translatable="yes" context="indicators-dialog">Select indicator</property> | |
38 | <property name="modal">True</property> | |
39 | <property name="window_position">center</property> | |
40 | <property name="destroy_with_parent">True</property> | |
41 | <property name="icon_name">application-x-executable</property> | |
42 | <property name="type_hint">dialog</property> | |
43 | <child internal-child="vbox"> | |
44 | <object class="GtkBox" id="dialog-vbox1"> | |
45 | <property name="can_focus">False</property> | |
46 | <property name="margin_left">8</property> | |
47 | <property name="margin_right">8</property> | |
48 | <property name="margin_top">8</property> | |
49 | <property name="margin_bottom">8</property> | |
50 | <property name="orientation">vertical</property> | |
51 | <property name="spacing">8</property> | |
52 | <child internal-child="action_area"> | |
53 | <object class="GtkButtonBox" id="dialog-action_area1"> | |
54 | <property name="can_focus">False</property> | |
55 | <property name="layout_style">end</property> | |
56 | <child> | |
57 | <object class="GtkButton" id="cancel_button"> | |
58 | <property name="label" translatable="yes" context="button">_Close</property> | |
59 | <property name="visible">True</property> | |
60 | <property name="can_focus">True</property> | |
61 | <property name="receives_default">True</property> | |
62 | <property name="image">image2</property> | |
63 | <property name="use_underline">True</property> | |
64 | </object> | |
65 | <packing> | |
66 | <property name="expand">False</property> | |
67 | <property name="fill">True</property> | |
68 | <property name="position">0</property> | |
69 | </packing> | |
70 | </child> | |
71 | <child> | |
72 | <object class="GtkButton" id="ok_button"> | |
73 | <property name="label" translatable="yes" context="button">_OK</property> | |
74 | <property name="visible">True</property> | |
75 | <property name="can_focus">True</property> | |
76 | <property name="can_default">True</property> | |
77 | <property name="has_default">True</property> | |
78 | <property name="receives_default">True</property> | |
79 | <property name="tooltip_text" translatable="yes">Add selected indicator to the list and close this window</property> | |
80 | <property name="image">image3</property> | |
81 | <property name="use_underline">True</property> | |
82 | </object> | |
83 | <packing> | |
84 | <property name="expand">False</property> | |
85 | <property name="fill">True</property> | |
86 | <property name="position">1</property> | |
87 | </packing> | |
88 | </child> | |
89 | <child> | |
90 | <object class="GtkBox" id="box1"> | |
91 | <property name="visible">True</property> | |
92 | <property name="can_focus">False</property> | |
93 | <property name="orientation">vertical</property> | |
94 | <child> | |
95 | <object class="GtkButton" id="add_button"> | |
96 | <property name="label" translatable="yes" context="button">_Add</property> | |
97 | <property name="visible">True</property> | |
98 | <property name="can_focus">True</property> | |
99 | <property name="receives_default">True</property> | |
100 | <property name="has_tooltip">True</property> | |
101 | <property name="tooltip_text" translatable="yes">Add selected indicator to the list without closing this window</property> | |
102 | <property name="image">image1</property> | |
103 | <property name="use_underline">True</property> | |
104 | <signal name="clicked" handler="on_add_clicked" swapped="no"/> | |
105 | </object> | |
106 | <packing> | |
107 | <property name="expand">False</property> | |
108 | <property name="fill">True</property> | |
109 | <property name="position">0</property> | |
110 | </packing> | |
111 | </child> | |
112 | </object> | |
113 | <packing> | |
114 | <property name="expand">False</property> | |
115 | <property name="fill">True</property> | |
116 | <property name="position">2</property> | |
117 | <property name="secondary">True</property> | |
118 | <property name="non_homogeneous">True</property> | |
119 | </packing> | |
120 | </child> | |
121 | </object> | |
122 | <packing> | |
123 | <property name="expand">False</property> | |
124 | <property name="fill">True</property> | |
125 | <property name="pack_type">end</property> | |
126 | <property name="position">0</property> | |
127 | </packing> | |
128 | </child> | |
129 | <child> | |
130 | <object class="GtkBox" id="box7"> | |
131 | <property name="visible">True</property> | |
132 | <property name="can_focus">False</property> | |
133 | <property name="valign">start</property> | |
134 | <property name="hexpand">True</property> | |
135 | <property name="orientation">vertical</property> | |
136 | <property name="spacing">8</property> | |
137 | <child> | |
138 | <object class="GtkRadioButton" id="short_choice"> | |
139 | <property name="visible">True</property> | |
140 | <property name="can_focus">True</property> | |
141 | <property name="receives_default">False</property> | |
142 | <property name="xalign">0</property> | |
143 | <property name="active">True</property> | |
144 | <property name="draw_indicator">True</property> | |
145 | <signal name="toggled" handler="on_short_choice_toggled" swapped="no"/> | |
146 | <child> | |
147 | <object class="GtkLabel" id="label5"> | |
148 | <property name="visible">True</property> | |
149 | <property name="can_focus">False</property> | |
150 | <property name="halign">start</property> | |
151 | <property name="hexpand">True</property> | |
152 | <property name="label" translatable="yes" context="indicators-dialog">Short name</property> | |
153 | <attributes> | |
154 | <attribute name="weight" value="semibold"/> | |
155 | </attributes> | |
156 | </object> | |
157 | </child> | |
158 | </object> | |
159 | <packing> | |
160 | <property name="expand">False</property> | |
161 | <property name="fill">True</property> | |
162 | <property name="position">0</property> | |
163 | </packing> | |
164 | </child> | |
165 | <child> | |
166 | <object class="GtkComboBox" id="combobox1"> | |
167 | <property name="visible">True</property> | |
168 | <property name="can_focus">False</property> | |
169 | <property name="margin_left">24</property> | |
170 | <property name="model">short_model</property> | |
171 | <property name="has_entry">True</property> | |
172 | <property name="entry_text_column">0</property> | |
173 | <child internal-child="entry"> | |
174 | <object class="GtkEntry" id="short_value"> | |
175 | <property name="can_focus">True</property> | |
176 | <property name="is_focus">True</property> | |
177 | <signal name="activate" handler="on_short_value_activate" swapped="no"/> | |
178 | <signal name="changed" handler="on_short_value_changed" swapped="no"/> | |
179 | </object> | |
180 | </child> | |
181 | </object> | |
182 | <packing> | |
183 | <property name="expand">False</property> | |
184 | <property name="fill">True</property> | |
185 | <property name="position">1</property> | |
186 | </packing> | |
187 | </child> | |
188 | <child> | |
189 | <object class="GtkLabel" id="label17"> | |
190 | <property name="visible">True</property> | |
191 | <property name="can_focus">False</property> | |
192 | <property name="halign">start</property> | |
193 | <property name="margin_left">24</property> | |
194 | <property name="label" translatable="yes" context="indicators-dialog">Indicators that greeter can find without using absolute pathes</property> | |
195 | <attributes> | |
196 | <attribute name="style" value="oblique"/> | |
197 | </attributes> | |
198 | </object> | |
199 | <packing> | |
200 | <property name="expand">False</property> | |
201 | <property name="fill">True</property> | |
202 | <property name="position">2</property> | |
203 | </packing> | |
204 | </child> | |
205 | <child> | |
206 | <object class="GtkRadioButton" id="path_choice"> | |
207 | <property name="visible">True</property> | |
208 | <property name="can_focus">True</property> | |
209 | <property name="receives_default">False</property> | |
210 | <property name="xalign">0</property> | |
211 | <property name="draw_indicator">True</property> | |
212 | <property name="group">short_choice</property> | |
213 | <child> | |
214 | <object class="GtkLabel" id="label4"> | |
215 | <property name="visible">True</property> | |
216 | <property name="can_focus">False</property> | |
217 | <property name="halign">start</property> | |
218 | <property name="hexpand">True</property> | |
219 | <property name="label" translatable="yes" context="indicators-dialog">Path</property> | |
220 | <attributes> | |
221 | <attribute name="weight" value="semibold"/> | |
222 | </attributes> | |
223 | </object> | |
224 | </child> | |
225 | </object> | |
226 | <packing> | |
227 | <property name="expand">False</property> | |
228 | <property name="fill">True</property> | |
229 | <property name="position">3</property> | |
230 | </packing> | |
231 | </child> | |
232 | <child> | |
233 | <object class="GtkFileChooserButton" id="path_value"> | |
234 | <property name="visible">True</property> | |
235 | <property name="can_focus">False</property> | |
236 | <property name="margin_left">24</property> | |
237 | <property name="filter">library_file_filter</property> | |
238 | <signal name="file-set" handler="on_path_value_changed" swapped="no"/> | |
239 | </object> | |
240 | <packing> | |
241 | <property name="expand">False</property> | |
242 | <property name="fill">True</property> | |
243 | <property name="position">4</property> | |
244 | </packing> | |
245 | </child> | |
246 | <child> | |
247 | <object class="GtkLabel" id="label16"> | |
248 | <property name="visible">True</property> | |
249 | <property name="can_focus">False</property> | |
250 | <property name="halign">start</property> | |
251 | <property name="margin_left">24</property> | |
252 | <property name="label" translatable="yes" context="indicators-dialog">Absolute path to indicator library file (*.so)</property> | |
253 | <attributes> | |
254 | <attribute name="style" value="oblique"/> | |
255 | </attributes> | |
256 | </object> | |
257 | <packing> | |
258 | <property name="expand">False</property> | |
259 | <property name="fill">True</property> | |
260 | <property name="position">5</property> | |
261 | </packing> | |
262 | </child> | |
263 | <child> | |
264 | <object class="GtkSeparator" id="separator2"> | |
265 | <property name="visible">True</property> | |
266 | <property name="can_focus">False</property> | |
267 | </object> | |
268 | <packing> | |
269 | <property name="expand">False</property> | |
270 | <property name="fill">True</property> | |
271 | <property name="position">6</property> | |
272 | </packing> | |
273 | </child> | |
274 | <child> | |
275 | <object class="GtkGrid" id="grid1"> | |
276 | <property name="visible">True</property> | |
277 | <property name="can_focus">False</property> | |
278 | <property name="halign">start</property> | |
279 | <property name="row_spacing">4</property> | |
280 | <property name="column_spacing">8</property> | |
281 | <child> | |
282 | <object class="GtkLabel" id="label2"> | |
283 | <property name="visible">True</property> | |
284 | <property name="can_focus">False</property> | |
285 | <property name="halign">start</property> | |
286 | <property name="label" translatable="yes" context="indicators-dialog">Fills the maximum available space</property> | |
287 | <attributes> | |
288 | <attribute name="style" value="oblique"/> | |
289 | </attributes> | |
290 | </object> | |
291 | <packing> | |
292 | <property name="left_attach">1</property> | |
293 | <property name="top_attach">0</property> | |
294 | </packing> | |
295 | </child> | |
296 | <child> | |
297 | <object class="GtkRadioButton" id="spacer_choice"> | |
298 | <property name="visible">True</property> | |
299 | <property name="can_focus">True</property> | |
300 | <property name="receives_default">False</property> | |
301 | <property name="xalign">0</property> | |
302 | <property name="draw_indicator">True</property> | |
303 | <property name="group">short_choice</property> | |
304 | <child> | |
305 | <object class="GtkLabel" id="label1"> | |
306 | <property name="visible">True</property> | |
307 | <property name="can_focus">False</property> | |
308 | <property name="halign">start</property> | |
309 | <property name="hexpand">True</property> | |
310 | <property name="label" translatable="yes" context="indicators-dialog">Spacer</property> | |
311 | <attributes> | |
312 | <attribute name="weight" value="semibold"/> | |
313 | </attributes> | |
314 | </object> | |
315 | </child> | |
316 | </object> | |
317 | <packing> | |
318 | <property name="left_attach">0</property> | |
319 | <property name="top_attach">0</property> | |
320 | </packing> | |
321 | </child> | |
322 | <child> | |
323 | <object class="GtkRadioButton" id="separator_choice"> | |
324 | <property name="visible">True</property> | |
325 | <property name="can_focus">True</property> | |
326 | <property name="receives_default">False</property> | |
327 | <property name="xalign">0</property> | |
328 | <property name="draw_indicator">True</property> | |
329 | <property name="group">short_choice</property> | |
330 | <child> | |
331 | <object class="GtkLabel" id="label3"> | |
332 | <property name="visible">True</property> | |
333 | <property name="can_focus">False</property> | |
334 | <property name="halign">start</property> | |
335 | <property name="hexpand">True</property> | |
336 | <property name="label" translatable="yes" context="indicators-dialog">Separator</property> | |
337 | <attributes> | |
338 | <attribute name="weight" value="semibold"/> | |
339 | </attributes> | |
340 | </object> | |
341 | </child> | |
342 | </object> | |
343 | <packing> | |
344 | <property name="left_attach">0</property> | |
345 | <property name="top_attach">1</property> | |
346 | </packing> | |
347 | </child> | |
348 | <child> | |
349 | <object class="GtkLabel" id="label6"> | |
350 | <property name="visible">True</property> | |
351 | <property name="can_focus">False</property> | |
352 | <property name="halign">start</property> | |
353 | <property name="label" translatable="yes" context="indicators-dialog">Draw a separator</property> | |
354 | <attributes> | |
355 | <attribute name="style" value="oblique"/> | |
356 | </attributes> | |
357 | </object> | |
358 | <packing> | |
359 | <property name="left_attach">1</property> | |
360 | <property name="top_attach">1</property> | |
361 | </packing> | |
362 | </child> | |
363 | </object> | |
364 | <packing> | |
365 | <property name="expand">False</property> | |
366 | <property name="fill">True</property> | |
367 | <property name="position">7</property> | |
368 | </packing> | |
369 | </child> | |
370 | <child> | |
371 | <object class="GtkInfoBar" id="infobar"> | |
372 | <property name="visible">True</property> | |
373 | <property name="app_paintable">True</property> | |
374 | <property name="can_focus">False</property> | |
375 | <property name="message_type">warning</property> | |
376 | <child internal-child="action_area"> | |
377 | <object class="GtkButtonBox" id="infobar-action_area1"> | |
378 | <property name="can_focus">False</property> | |
379 | <property name="border_width">5</property> | |
380 | <property name="orientation">vertical</property> | |
381 | <property name="layout_style">start</property> | |
382 | <child> | |
383 | <placeholder/> | |
384 | </child> | |
385 | </object> | |
386 | <packing> | |
387 | <property name="expand">False</property> | |
388 | <property name="fill">True</property> | |
389 | <property name="position">1</property> | |
390 | </packing> | |
391 | </child> | |
392 | <child internal-child="content_area"> | |
393 | <object class="GtkBox" id="infobar-content_area1"> | |
394 | <property name="can_focus">False</property> | |
395 | <property name="border_width">8</property> | |
396 | <property name="spacing">16</property> | |
397 | <child> | |
398 | <object class="GtkLabel" id="message"> | |
399 | <property name="visible">True</property> | |
400 | <property name="can_focus">False</property> | |
401 | <property name="label">[message]</property> | |
402 | </object> | |
403 | <packing> | |
404 | <property name="expand">True</property> | |
405 | <property name="fill">True</property> | |
406 | <property name="position">0</property> | |
407 | </packing> | |
408 | </child> | |
409 | </object> | |
410 | <packing> | |
411 | <property name="expand">True</property> | |
412 | <property name="fill">True</property> | |
413 | <property name="position">0</property> | |
414 | </packing> | |
415 | </child> | |
416 | </object> | |
417 | <packing> | |
418 | <property name="expand">False</property> | |
419 | <property name="fill">True</property> | |
420 | <property name="position">8</property> | |
421 | </packing> | |
422 | </child> | |
423 | </object> | |
424 | <packing> | |
425 | <property name="expand">True</property> | |
426 | <property name="fill">True</property> | |
427 | <property name="position">1</property> | |
428 | </packing> | |
429 | </child> | |
430 | <child> | |
431 | <object class="GtkSeparator" id="separator1"> | |
432 | <property name="visible">True</property> | |
433 | <property name="can_focus">False</property> | |
434 | </object> | |
435 | <packing> | |
436 | <property name="expand">False</property> | |
437 | <property name="fill">True</property> | |
438 | <property name="position">2</property> | |
439 | </packing> | |
440 | </child> | |
441 | </object> | |
442 | </child> | |
443 | <action-widgets> | |
444 | <action-widget response="-7">cancel_button</action-widget> | |
445 | <action-widget response="-5">ok_button</action-widget> | |
446 | </action-widgets> | |
447 | </object> | |
448 | </interface> |
0 | <?xml version="1.0" encoding="UTF-8"?> | |
1 | <!-- Generated with glade 3.16.1 --> | |
2 | <interface> | |
3 | <requires lib="gtk+" version="3.8"/> | |
4 | <!-- interface-requires gtk_greeter_settings 1.0 --> | |
5 | <object class="GtkImage" id="image1"> | |
6 | <property name="visible">True</property> | |
7 | <property name="can_focus">False</property> | |
8 | <property name="xpad">2</property> | |
9 | <property name="icon_name">list-add</property> | |
10 | </object> | |
11 | <object class="GtkImage" id="image2"> | |
12 | <property name="visible">True</property> | |
13 | <property name="can_focus">False</property> | |
14 | <property name="xpad">2</property> | |
15 | <property name="icon_name">window-close</property> | |
16 | </object> | |
17 | <object class="GtkImage" id="image3"> | |
18 | <property name="visible">True</property> | |
19 | <property name="can_focus">False</property> | |
20 | <property name="xpad">2</property> | |
21 | <property name="icon_name">dialog-ok</property> | |
22 | </object> | |
23 | <object class="GtkFileFilter" id="library_file_filter"> | |
24 | <mime-types> | |
25 | <mime-type>application/x-sharedlib</mime-type> | |
26 | </mime-types> | |
27 | </object> | |
28 | <object class="GtkMenu" id="option_image_menu"> | |
29 | <property name="visible">True</property> | |
30 | <property name="can_focus">False</property> | |
31 | <child> | |
32 | <object class="GtkMenuItem" id="option_image_default_item"> | |
33 | <property name="visible">True</property> | |
34 | <property name="can_focus">False</property> | |
35 | <property name="label">[default]</property> | |
36 | <property name="use_underline">True</property> | |
37 | </object> | |
38 | </child> | |
39 | <child> | |
40 | <object class="GtkMenuItem" id="option_image_icon_item"> | |
41 | <property name="visible">True</property> | |
42 | <property name="can_focus">False</property> | |
43 | <property name="label">[icon]</property> | |
44 | </object> | |
45 | </child> | |
46 | <child> | |
47 | <object class="GtkMenuItem" id="option_image_path_item"> | |
48 | <property name="visible">True</property> | |
49 | <property name="can_focus">False</property> | |
50 | <property name="label">[path]</property> | |
51 | </object> | |
52 | </child> | |
53 | </object> | |
54 | <object class="GtkListStore" id="option_path_model"> | |
55 | <columns> | |
56 | <!-- column-name title --> | |
57 | <column type="gchararray"/> | |
58 | <!-- column-name type --> | |
59 | <column type="gchararray"/> | |
60 | <!-- column-name icon --> | |
61 | <column type="gchararray"/> | |
62 | </columns> | |
63 | <data> | |
64 | <row> | |
65 | <col id="0" translatable="yes" context="option-entry|indicators">Select file...</col> | |
66 | <col id="1">select-path</col> | |
67 | <col id="2">system-search</col> | |
68 | </row> | |
69 | <row> | |
70 | <col id="0">-</col> | |
71 | <col id="1">separator</col> | |
72 | <col id="2">-</col> | |
73 | </row> | |
74 | </data> | |
75 | </object> | |
76 | <object class="IndicatorPropertiesDialog" id="indicator_properties_dialog"> | |
77 | <property name="width_request">400</property> | |
78 | <property name="can_focus">False</property> | |
79 | <property name="title" translatable="yes" context="option-entry|indicators">Indicator properties</property> | |
80 | <property name="resizable">False</property> | |
81 | <property name="modal">True</property> | |
82 | <property name="window_position">center</property> | |
83 | <property name="destroy_with_parent">True</property> | |
84 | <property name="icon_name">application-x-executable</property> | |
85 | <property name="type_hint">dialog</property> | |
86 | <child internal-child="vbox"> | |
87 | <object class="GtkBox" id="dialog-vbox1"> | |
88 | <property name="can_focus">False</property> | |
89 | <property name="margin_left">8</property> | |
90 | <property name="margin_right">8</property> | |
91 | <property name="margin_top">8</property> | |
92 | <property name="margin_bottom">8</property> | |
93 | <property name="orientation">vertical</property> | |
94 | <property name="spacing">8</property> | |
95 | <child> | |
96 | <object class="GtkBox" id="box2"> | |
97 | <property name="visible">True</property> | |
98 | <property name="can_focus">False</property> | |
99 | <property name="spacing">6</property> | |
100 | <child> | |
101 | <object class="GtkRadioButton" id="option_type_indicator_choice"> | |
102 | <property name="label" translatable="yes" context="option-entry|indicators">Indicator</property> | |
103 | <property name="visible">True</property> | |
104 | <property name="can_focus">True</property> | |
105 | <property name="receives_default">False</property> | |
106 | <property name="xalign">0</property> | |
107 | <property name="active">True</property> | |
108 | <property name="draw_indicator">True</property> | |
109 | </object> | |
110 | <packing> | |
111 | <property name="expand">False</property> | |
112 | <property name="fill">True</property> | |
113 | <property name="position">0</property> | |
114 | </packing> | |
115 | </child> | |
116 | <child> | |
117 | <object class="GtkComboBoxText" id="option_type_types"> | |
118 | <property name="visible">True</property> | |
119 | <property name="can_focus">False</property> | |
120 | <signal name="changed" handler="on_option_type_types_changed" swapped="no"/> | |
121 | </object> | |
122 | <packing> | |
123 | <property name="expand">False</property> | |
124 | <property name="fill">True</property> | |
125 | <property name="position">1</property> | |
126 | </packing> | |
127 | </child> | |
128 | </object> | |
129 | <packing> | |
130 | <property name="expand">False</property> | |
131 | <property name="fill">True</property> | |
132 | <property name="position">0</property> | |
133 | </packing> | |
134 | </child> | |
135 | <child internal-child="action_area"> | |
136 | <object class="GtkButtonBox" id="dialog-action_area1"> | |
137 | <property name="can_focus">False</property> | |
138 | <property name="homogeneous">True</property> | |
139 | <property name="layout_style">end</property> | |
140 | <child> | |
141 | <object class="GtkButton" id="cancel_button"> | |
142 | <property name="label" translatable="yes" context="button">_Close</property> | |
143 | <property name="visible">True</property> | |
144 | <property name="can_focus">True</property> | |
145 | <property name="receives_default">True</property> | |
146 | <property name="image">image2</property> | |
147 | <property name="use_underline">True</property> | |
148 | </object> | |
149 | <packing> | |
150 | <property name="expand">False</property> | |
151 | <property name="fill">True</property> | |
152 | <property name="position">0</property> | |
153 | </packing> | |
154 | </child> | |
155 | <child> | |
156 | <object class="GtkButton" id="ok_button"> | |
157 | <property name="label" translatable="yes" context="button">_OK</property> | |
158 | <property name="visible">True</property> | |
159 | <property name="can_focus">True</property> | |
160 | <property name="can_default">True</property> | |
161 | <property name="has_default">True</property> | |
162 | <property name="receives_default">True</property> | |
163 | <property name="image">image3</property> | |
164 | <property name="use_underline">True</property> | |
165 | </object> | |
166 | <packing> | |
167 | <property name="expand">False</property> | |
168 | <property name="fill">True</property> | |
169 | <property name="position">1</property> | |
170 | </packing> | |
171 | </child> | |
172 | <child> | |
173 | <object class="GtkBox" id="box1"> | |
174 | <property name="visible">True</property> | |
175 | <property name="can_focus">False</property> | |
176 | <property name="orientation">vertical</property> | |
177 | <property name="homogeneous">True</property> | |
178 | <child> | |
179 | <object class="GtkButton" id="add_button"> | |
180 | <property name="label" translatable="yes" context="button">_Add</property> | |
181 | <property name="visible">True</property> | |
182 | <property name="can_focus">True</property> | |
183 | <property name="receives_default">True</property> | |
184 | <property name="image">image1</property> | |
185 | <property name="use_underline">True</property> | |
186 | <signal name="clicked" handler="on_add_clicked" swapped="no"/> | |
187 | </object> | |
188 | <packing> | |
189 | <property name="expand">False</property> | |
190 | <property name="fill">True</property> | |
191 | <property name="position">0</property> | |
192 | </packing> | |
193 | </child> | |
194 | </object> | |
195 | <packing> | |
196 | <property name="expand">False</property> | |
197 | <property name="fill">True</property> | |
198 | <property name="position">2</property> | |
199 | <property name="secondary">True</property> | |
200 | <property name="non_homogeneous">True</property> | |
201 | </packing> | |
202 | </child> | |
203 | </object> | |
204 | <packing> | |
205 | <property name="expand">False</property> | |
206 | <property name="fill">True</property> | |
207 | <property name="pack_type">end</property> | |
208 | <property name="position">0</property> | |
209 | </packing> | |
210 | </child> | |
211 | <child> | |
212 | <object class="GtkBox" id="option_type_indicator_box"> | |
213 | <property name="visible">True</property> | |
214 | <property name="can_focus">False</property> | |
215 | <property name="margin_left">24</property> | |
216 | <property name="orientation">vertical</property> | |
217 | <property name="spacing">4</property> | |
218 | <child> | |
219 | <object class="GtkGrid" id="grid1"> | |
220 | <property name="visible">True</property> | |
221 | <property name="can_focus">False</property> | |
222 | <property name="row_spacing">8</property> | |
223 | <property name="column_spacing">6</property> | |
224 | <child> | |
225 | <object class="GtkCheckButton" id="option_text_use"> | |
226 | <property name="label" translatable="yes" context="option-entry|indicators">Display label</property> | |
227 | <property name="visible">True</property> | |
228 | <property name="can_focus">True</property> | |
229 | <property name="receives_default">False</property> | |
230 | <property name="xalign">0</property> | |
231 | <property name="draw_indicator">True</property> | |
232 | </object> | |
233 | <packing> | |
234 | <property name="left_attach">0</property> | |
235 | <property name="top_attach">0</property> | |
236 | <property name="width">1</property> | |
237 | <property name="height">1</property> | |
238 | </packing> | |
239 | </child> | |
240 | <child> | |
241 | <object class="GtkEntry" id="option_text_value"> | |
242 | <property name="visible">True</property> | |
243 | <property name="can_focus">True</property> | |
244 | <property name="hexpand">True</property> | |
245 | <property name="placeholder_text" translatable="yes" context="option-entry|indicators">Leave empty to use default value</property> | |
246 | </object> | |
247 | <packing> | |
248 | <property name="left_attach">1</property> | |
249 | <property name="top_attach">0</property> | |
250 | <property name="width">1</property> | |
251 | <property name="height">1</property> | |
252 | </packing> | |
253 | </child> | |
254 | <child> | |
255 | <object class="GtkCheckButton" id="option_image_use"> | |
256 | <property name="label" translatable="yes" context="option-entry|indicators">Display image</property> | |
257 | <property name="visible">True</property> | |
258 | <property name="can_focus">True</property> | |
259 | <property name="receives_default">False</property> | |
260 | <property name="xalign">0</property> | |
261 | <property name="draw_indicator">True</property> | |
262 | </object> | |
263 | <packing> | |
264 | <property name="left_attach">0</property> | |
265 | <property name="top_attach">1</property> | |
266 | <property name="width">1</property> | |
267 | <property name="height">1</property> | |
268 | </packing> | |
269 | </child> | |
270 | <child> | |
271 | <object class="GtkMenuButton" id="option_image_button"> | |
272 | <property name="visible">True</property> | |
273 | <property name="can_focus">True</property> | |
274 | <property name="receives_default">True</property> | |
275 | <property name="halign">start</property> | |
276 | <property name="popup">option_image_menu</property> | |
277 | <child> | |
278 | <object class="GtkBox" id="box3"> | |
279 | <property name="visible">True</property> | |
280 | <property name="can_focus">False</property> | |
281 | <property name="spacing">4</property> | |
282 | <child> | |
283 | <object class="GtkImage" id="option_image_image"> | |
284 | <property name="width_request">24</property> | |
285 | <property name="height_request">24</property> | |
286 | <property name="visible">True</property> | |
287 | <property name="can_focus">False</property> | |
288 | <property name="pixel_size">24</property> | |
289 | <property name="icon_name">avatar-default</property> | |
290 | <property name="icon_size">1</property> | |
291 | </object> | |
292 | <packing> | |
293 | <property name="expand">False</property> | |
294 | <property name="fill">True</property> | |
295 | <property name="position">0</property> | |
296 | </packing> | |
297 | </child> | |
298 | <child> | |
299 | <object class="GtkLabel" id="option_image_label"> | |
300 | <property name="height_request">24</property> | |
301 | <property name="visible">True</property> | |
302 | <property name="can_focus">False</property> | |
303 | <property name="label">[icon]</property> | |
304 | </object> | |
305 | <packing> | |
306 | <property name="expand">False</property> | |
307 | <property name="fill">True</property> | |
308 | <property name="position">1</property> | |
309 | </packing> | |
310 | </child> | |
311 | </object> | |
312 | </child> | |
313 | </object> | |
314 | <packing> | |
315 | <property name="left_attach">1</property> | |
316 | <property name="top_attach">1</property> | |
317 | <property name="width">1</property> | |
318 | <property name="height">1</property> | |
319 | </packing> | |
320 | </child> | |
321 | </object> | |
322 | <packing> | |
323 | <property name="expand">False</property> | |
324 | <property name="fill">True</property> | |
325 | <property name="position">0</property> | |
326 | </packing> | |
327 | </child> | |
328 | <child> | |
329 | <object class="GtkNotebook" id="custom_options_box"> | |
330 | <property name="visible">True</property> | |
331 | <property name="can_focus">False</property> | |
332 | <property name="valign">start</property> | |
333 | <property name="show_tabs">False</property> | |
334 | <property name="show_border">False</property> | |
335 | <child> | |
336 | <object class="GtkBox" id="custom_options_external"> | |
337 | <property name="can_focus">False</property> | |
338 | <property name="orientation">vertical</property> | |
339 | <property name="spacing">4</property> | |
340 | <child> | |
341 | <object class="GtkLabel" id="label2"> | |
342 | <property name="visible">True</property> | |
343 | <property name="can_focus">False</property> | |
344 | <property name="halign">start</property> | |
345 | <property name="label" translatable="yes" context="option-entry|indicators">Indicator library/service:</property> | |
346 | </object> | |
347 | <packing> | |
348 | <property name="expand">False</property> | |
349 | <property name="fill">True</property> | |
350 | <property name="position">0</property> | |
351 | </packing> | |
352 | </child> | |
353 | <child> | |
354 | <object class="GtkComboBox" id="option_path_combo"> | |
355 | <property name="visible">True</property> | |
356 | <property name="can_focus">False</property> | |
357 | <property name="model">option_path_model</property> | |
358 | <property name="has_entry">True</property> | |
359 | <property name="entry_text_column">0</property> | |
360 | <property name="id_column">1</property> | |
361 | <child> | |
362 | <object class="GtkCellRendererPixbuf" id="cellrenderertext1"/> | |
363 | <attributes> | |
364 | <attribute name="icon-name">2</attribute> | |
365 | </attributes> | |
366 | </child> | |
367 | <child> | |
368 | <object class="GtkCellRendererText" id="cellrenderertext2"/> | |
369 | <attributes> | |
370 | <attribute name="text">0</attribute> | |
371 | </attributes> | |
372 | </child> | |
373 | <child internal-child="entry"> | |
374 | <object class="GtkEntry" id="option_path_entry"> | |
375 | <property name="can_focus">True</property> | |
376 | </object> | |
377 | </child> | |
378 | </object> | |
379 | <packing> | |
380 | <property name="expand">False</property> | |
381 | <property name="fill">True</property> | |
382 | <property name="position">1</property> | |
383 | </packing> | |
384 | </child> | |
385 | </object> | |
386 | </child> | |
387 | <child type="tab"> | |
388 | <object class="GtkLabel" id="label3"> | |
389 | <property name="visible">True</property> | |
390 | <property name="can_focus">False</property> | |
391 | <property name="label" translatable="yes">page 1</property> | |
392 | </object> | |
393 | <packing> | |
394 | <property name="tab_fill">False</property> | |
395 | </packing> | |
396 | </child> | |
397 | <child> | |
398 | <object class="GtkBox" id="options_power"> | |
399 | <property name="can_focus">False</property> | |
400 | <property name="orientation">vertical</property> | |
401 | <child> | |
402 | <object class="GtkCheckButton" id="option_hide_disabled_value"> | |
403 | <property name="label" translatable="yes" context="option-entry|indicators">Hide disabled power actions</property> | |
404 | <property name="visible">True</property> | |
405 | <property name="can_focus">True</property> | |
406 | <property name="receives_default">False</property> | |
407 | <property name="xalign">0</property> | |
408 | <property name="draw_indicator">True</property> | |
409 | </object> | |
410 | <packing> | |
411 | <property name="expand">False</property> | |
412 | <property name="fill">True</property> | |
413 | <property name="position">0</property> | |
414 | </packing> | |
415 | </child> | |
416 | </object> | |
417 | <packing> | |
418 | <property name="position">1</property> | |
419 | </packing> | |
420 | </child> | |
421 | <child type="tab"> | |
422 | <object class="GtkLabel" id="label1"> | |
423 | <property name="visible">True</property> | |
424 | <property name="can_focus">False</property> | |
425 | <property name="label" translatable="yes">page 2</property> | |
426 | </object> | |
427 | <packing> | |
428 | <property name="position">1</property> | |
429 | <property name="tab_fill">False</property> | |
430 | </packing> | |
431 | </child> | |
432 | </object> | |
433 | <packing> | |
434 | <property name="expand">False</property> | |
435 | <property name="fill">True</property> | |
436 | <property name="position">1</property> | |
437 | </packing> | |
438 | </child> | |
439 | </object> | |
440 | <packing> | |
441 | <property name="expand">False</property> | |
442 | <property name="fill">True</property> | |
443 | <property name="position">2</property> | |
444 | </packing> | |
445 | </child> | |
446 | <child> | |
447 | <object class="GtkRadioButton" id="option_type_spacer_choice"> | |
448 | <property name="label" translatable="yes" context="option-entry|indicators">Spacer - fills the maximum available space</property> | |
449 | <property name="visible">True</property> | |
450 | <property name="can_focus">True</property> | |
451 | <property name="receives_default">False</property> | |
452 | <property name="xalign">0</property> | |
453 | <property name="draw_indicator">True</property> | |
454 | <property name="group">option_type_indicator_choice</property> | |
455 | </object> | |
456 | <packing> | |
457 | <property name="expand">False</property> | |
458 | <property name="fill">True</property> | |
459 | <property name="position">3</property> | |
460 | </packing> | |
461 | </child> | |
462 | <child> | |
463 | <object class="GtkRadioButton" id="option_type_separator_choice"> | |
464 | <property name="label" translatable="yes" context="option-entry|indicators">Separator - draw a separator</property> | |
465 | <property name="visible">True</property> | |
466 | <property name="can_focus">True</property> | |
467 | <property name="receives_default">False</property> | |
468 | <property name="xalign">0</property> | |
469 | <property name="draw_indicator">True</property> | |
470 | <property name="group">option_type_indicator_choice</property> | |
471 | </object> | |
472 | <packing> | |
473 | <property name="expand">False</property> | |
474 | <property name="fill">True</property> | |
475 | <property name="position">4</property> | |
476 | </packing> | |
477 | </child> | |
478 | <child> | |
479 | <object class="GtkInfoBar" id="infobar"> | |
480 | <property name="app_paintable">True</property> | |
481 | <property name="can_focus">False</property> | |
482 | <property name="message_type">warning</property> | |
483 | <child internal-child="action_area"> | |
484 | <object class="GtkButtonBox" id="infobar-action_area"> | |
485 | <property name="can_focus">False</property> | |
486 | <property name="orientation">vertical</property> | |
487 | <property name="layout_style">start</property> | |
488 | <child> | |
489 | <placeholder/> | |
490 | </child> | |
491 | </object> | |
492 | <packing> | |
493 | <property name="expand">False</property> | |
494 | <property name="fill">True</property> | |
495 | <property name="position">1</property> | |
496 | </packing> | |
497 | </child> | |
498 | <child internal-child="content_area"> | |
499 | <object class="GtkBox" id="infobar-content_area"> | |
500 | <property name="can_focus">False</property> | |
501 | <property name="spacing">16</property> | |
502 | <child> | |
503 | <object class="GtkLabel" id="message"> | |
504 | <property name="visible">True</property> | |
505 | <property name="can_focus">False</property> | |
506 | <property name="label">[message]</property> | |
507 | </object> | |
508 | <packing> | |
509 | <property name="expand">True</property> | |
510 | <property name="fill">True</property> | |
511 | <property name="position">0</property> | |
512 | </packing> | |
513 | </child> | |
514 | </object> | |
515 | <packing> | |
516 | <property name="expand">True</property> | |
517 | <property name="fill">True</property> | |
518 | <property name="position">0</property> | |
519 | </packing> | |
520 | </child> | |
521 | </object> | |
522 | <packing> | |
523 | <property name="expand">False</property> | |
524 | <property name="fill">True</property> | |
525 | <property name="position">5</property> | |
526 | </packing> | |
527 | </child> | |
528 | <child> | |
529 | <object class="GtkSeparator" id="separator1"> | |
530 | <property name="visible">True</property> | |
531 | <property name="can_focus">False</property> | |
532 | </object> | |
533 | <packing> | |
534 | <property name="expand">False</property> | |
535 | <property name="fill">True</property> | |
536 | <property name="position">6</property> | |
537 | </packing> | |
538 | </child> | |
539 | </object> | |
540 | </child> | |
541 | <action-widgets> | |
542 | <action-widget response="-7">cancel_button</action-widget> | |
543 | <action-widget response="-5">ok_button</action-widget> | |
544 | </action-widgets> | |
545 | </object> | |
546 | </interface> |
156 | 156 | <property name="headers_clickable">False</property> |
157 | 157 | <property name="search_column">0</property> |
158 | 158 | <child internal-child="selection"> |
159 | <object class="GtkTreeSelection" id="treeview-selection"> | |
159 | <object class="GtkTreeSelection" id="monitors_selection"> | |
160 | 160 | <signal name="changed" handler="on_selection_changed" swapped="no"/> |
161 | 161 | </object> |
162 | 162 | </child> |
3 | 3 | <glade-widget-class title="Settings Window" name="GtkGreeterSettingsWindow" |
4 | 4 | generic-name="GtkGreeterSettingsWindow" parent="GtkWindow" |
5 | 5 | icon-name="widget-gtk-window"/> |
6 | <glade-widget-class title="Indicator Chooser Dialog" name="IndicatorChooserDialog" | |
7 | generic-name="IndicatorChooserDialog" parent="GtkDialog" | |
6 | <glade-widget-class title="Indicator Properties Dialog" name="IndicatorPropertiesDialog" | |
7 | generic-name="IndicatorPropertiesDialog" parent="GtkDialog" | |
8 | 8 | icon-name="widget-gtk-dialog"/> |
9 | 9 | <glade-widget-class title="Icon Chooser Dialog" name="IconChooserDialog" |
10 | 10 | generic-name="IconChooserDialog" parent="GtkDialog" |
15 | 15 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 | 17 | |
18 | import collections | |
18 | 19 | import configparser |
19 | 20 | import os |
20 | 21 | import sys |
21 | from collections import namedtuple | |
22 | 22 | from glob import iglob |
23 | 23 | from itertools import chain |
24 | 24 | from locale import gettext as _ |
25 | from gi.repository import Gtk, Gdk | |
26 | ||
27 | from lightdm_gtk_greeter_settings import helpers | |
28 | from lightdm_gtk_greeter_settings.helpers import C_, WidgetsWrapper, string2bool | |
29 | from lightdm_gtk_greeter_settings import OptionEntry | |
25 | ||
26 | from gi.repository import ( | |
27 | Gdk, | |
28 | Gtk) | |
29 | ||
30 | from lightdm_gtk_greeter_settings import ( | |
31 | helpers, | |
32 | IndicatorsEntry, | |
33 | OptionEntry, | |
34 | PositionEntry) | |
35 | from lightdm_gtk_greeter_settings.helpers import ( | |
36 | C_, | |
37 | string2bool, | |
38 | WidgetsWrapper, WidgetsEnum) | |
39 | from lightdm_gtk_greeter_settings.MonitorsGroup import MonitorsGroup | |
30 | 40 | from lightdm_gtk_greeter_settings.OptionGroup import SimpleGroup |
31 | from lightdm_gtk_greeter_settings.MonitorsGroup import MonitorsGroup | |
32 | from lightdm_gtk_greeter_settings import PositionEntry | |
33 | 41 | |
34 | 42 | |
35 | 43 | __all__ = ['GtkGreeterSettingsWindow'] |
36 | 44 | |
37 | 45 | |
38 | InitialValue = namedtuple('InitialValue', ('value', 'enabled')) | |
46 | InitialValue = collections.namedtuple('InitialValue', ('value', 'enabled')) | |
39 | 47 | |
40 | 48 | |
41 | 49 | class GtkGreeterSettingsWindow(Gtk.Window): |
42 | 50 | |
43 | 51 | __gtype_name__ = 'GtkGreeterSettingsWindow' |
44 | 52 | |
45 | BUILDER_WIDGETS = ('apply_button', 'no_access_infobar', | |
46 | 'gtk_theme_values', 'icons_theme_values') | |
53 | class Widgets(WidgetsEnum): | |
54 | apply = 'apply_button' | |
55 | infobar = 'infobar' | |
47 | 56 | |
48 | 57 | def __new__(cls): |
49 | 58 | builder = Gtk.Builder() |
50 | 59 | builder.add_from_file(helpers.get_data_path('%s.ui' % cls.__name__)) |
51 | 60 | window = builder.get_object('settings_window') |
52 | window._builder = builder | |
53 | window._widgets = WidgetsWrapper(builder) | |
54 | window.__dict__.update(('_' + w, builder.get_object(w)) | |
55 | for w in cls.BUILDER_WIDGETS) | |
61 | window.builder = builder | |
56 | 62 | builder.connect_signals(window) |
57 | window._init_window() | |
63 | window.init_window() | |
58 | 64 | return window |
59 | 65 | |
60 | def _init_window(self): | |
61 | ||
62 | self._entries_setup = \ | |
63 | { | |
64 | ('greeter', 'screensaver-timeout'): self.on_entry_setup_greeter_screensaver_timeout, | |
65 | ('greeter', 'theme-name'): self.on_entry_setup_greeter_theme_name, | |
66 | ('greeter', 'icon-theme-name'): self.on_entry_setup_greeter_icon_theme_name, | |
67 | ('greeter', 'default-user-image'): self.on_entry_setup_greeter_default_user_image, | |
68 | ('greeter', 'background'): self.on_entry_setup_greeter_background, | |
69 | ('greeter', 'allow-debugging'): self.on_entry_setup_greeter_allow_debugging, | |
70 | } | |
66 | builder = None | |
67 | ||
68 | entries_setup = { | |
69 | ('greeter', 'allow-debugging'): ('changed',), | |
70 | ('greeter', 'background'): ('changed',), | |
71 | ('greeter', 'default-user-image'): ('changed',), | |
72 | ('greeter', 'screensaver-timeout'): ('setup', 'get', 'set'), | |
73 | ('greeter', 'theme-name'): ('setup', 'changed'), | |
74 | ('greeter', 'icon-theme-name'): ('setup', 'changed')} | |
75 | ||
76 | def init_window(self): | |
77 | self._widgets = self.Widgets(builder=self.builder) | |
71 | 78 | |
72 | 79 | self._multihead_dialog = None |
73 | 80 | self._initial_values = {} |
74 | 81 | self._changed_entries = None |
75 | 82 | self._entries = None |
76 | self._groups = \ | |
77 | ( | |
78 | SimpleGroup('greeter', self._builder, | |
79 | { | |
83 | self._groups = ( | |
84 | SimpleGroup('greeter', self.builder, { | |
80 | 85 | # Appearance |
81 | 86 | 'theme-name': (OptionEntry.StringEntry, None), |
82 | 87 | 'icon-theme-name': (OptionEntry.StringEntry, None), |
91 | 96 | 'default-user-image': (OptionEntry.IconEntry, '#avatar-default'), |
92 | 97 | # Panel |
93 | 98 | 'clock-format': (OptionEntry.ClockFormatEntry, '%a, %H:%M'), |
94 | 'indicators': (OptionEntry.IndicatorsEntry, | |
99 | 'indicators': (IndicatorsEntry.IndicatorsEntry, | |
95 | 100 | '~host;~spacer;~clock;~spacer;~language;~session;~a11y;~power'), |
96 | 101 | # Position |
97 | 102 | 'position': (PositionEntry.PositionEntry, '50%,center'), |
100 | 105 | 'keyboard': (OptionEntry.StringPathEntry, None), |
101 | 106 | 'reader': (OptionEntry.StringPathEntry, None), |
102 | 107 | 'a11y-states': (OptionEntry.AccessibilityStatesEntry, None), |
103 | 'allow-debugging': (OptionEntry.BooleanEntry, 'false'), | |
104 | }), | |
105 | MonitorsGroup(self._widgets) | |
106 | ) | |
108 | 'allow-debugging': (OptionEntry.BooleanEntry, 'false'), }), | |
109 | MonitorsGroup(WidgetsWrapper(self.builder))) | |
107 | 110 | |
108 | 111 | for group in self._groups: |
109 | 112 | group.entry_added.connect(self.on_entry_added) |
110 | 113 | group.entry_removed.connect(self.on_entry_removed) |
111 | 114 | |
112 | self._timeout_adjustment = self._widgets['greeter', 'screensaver-timeout', 'adjustment'] | |
113 | ||
114 | 115 | self._config_path = helpers.get_config_path() |
115 | 116 | self._allow_edit = self._has_access_to_write(self._config_path) |
116 | self._no_access_infobar.props.visible = not self._allow_edit | |
117 | self._apply_button.props.visible = self._allow_edit | |
117 | self._widgets.infobar.props.visible = not self._allow_edit | |
118 | self._widgets.apply.props.visible = self._allow_edit | |
118 | 119 | if not self._allow_edit: |
119 | 120 | helpers.show_message( |
120 | 121 | text=_('No permissions to save configuration'), |
150 | 151 | group.read(self._config) |
151 | 152 | |
152 | 153 | self._initial_values = {entry: InitialValue(entry.value, entry.enabled) |
153 | for entry in self._initial_values.keys()} | |
154 | for entry in self._initial_values.keys()} | |
154 | 155 | self._changed_entries = set() |
155 | self._apply_button.props.sensitive = False | |
156 | self._widgets.apply.props.sensitive = False | |
156 | 157 | |
157 | 158 | def _write(self): |
158 | 159 | for group in self._groups: |
162 | 163 | self._initial_values[entry] = InitialValue(entry.value, entry.enabled) |
163 | 164 | |
164 | 165 | self._changed_entries.clear() |
165 | self._apply_button.props.sensitive = False | |
166 | self._widgets.apply.props.sensitive = False | |
166 | 167 | |
167 | 168 | try: |
168 | 169 | with open(self._config_path, 'w') as file: |
171 | 172 | helpers.show_message(e, Gtk.MessageType.ERROR) |
172 | 173 | |
173 | 174 | def on_entry_added(self, group, entry, key): |
174 | if isinstance(group, SimpleGroup) and (group.name, key) in self._entries_setup: | |
175 | self._entries_setup[(group.name, key)](entry) | |
175 | if isinstance(group, SimpleGroup) and (group.name, key) in self.entries_setup: | |
176 | for action in self.entries_setup[(group.name, key)]: | |
177 | fname = 'on_entry_%s_%s_%s' % (action, group.name, key) | |
178 | f = getattr(self, fname.replace('-', '_')) | |
179 | if action == 'setup': | |
180 | f(entry) | |
181 | else: | |
182 | entry.connect(action, f) | |
183 | ||
176 | 184 | entry.changed.connect(self.on_entry_changed) |
177 | 185 | self._initial_values[entry] = InitialValue(entry.value, entry.enabled) |
178 | 186 | self.on_entry_changed(entry, force=True) |
183 | 191 | return |
184 | 192 | |
185 | 193 | self._changed_entries.discard(entry) |
186 | self._apply_button.props.sensitive = self._allow_edit and self._changed_entries | |
194 | self._widgets.apply.props.sensitive = self._allow_edit and self._changed_entries | |
187 | 195 | |
188 | 196 | def on_entry_changed(self, entry, force=False): |
189 | 197 | if self._changed_entries is None: |
190 | 198 | return |
191 | 199 | |
192 | if force or entry.enabled != self._initial_values[entry].enabled or \ | |
193 | (entry.enabled and entry.value != self._initial_values[entry].value): | |
200 | initial = self._initial_values[entry] | |
201 | if force or entry.enabled != initial.enabled or \ | |
202 | (entry.enabled and entry.value != initial.value): | |
194 | 203 | self._changed_entries.add(entry) |
195 | 204 | else: |
196 | 205 | self._changed_entries.discard(entry) |
197 | self._apply_button.props.sensitive = self._allow_edit and self._changed_entries | |
198 | ||
206 | ||
207 | self._widgets.apply.props.sensitive = self._allow_edit and self._changed_entries | |
208 | ||
209 | # [greeter] screensaver-timeout | |
199 | 210 | def on_entry_setup_greeter_screensaver_timeout(self, entry): |
200 | timeout_view = entry.widgets['view'] | |
201 | timeout_adjustment = entry.widgets['adjustment'] | |
202 | timeout_end_label = entry.widgets['end-label'] | |
203 | for mark in chain(list(range(10, 61, 10)), | |
204 | list(range(69, | |
205 | int(timeout_adjustment.props.upper), | |
206 | 10))): | |
207 | timeout_view.add_mark(mark, Gtk.PositionType.BOTTOM, None) | |
208 | total = int(timeout_adjustment.props.upper - 60) + 1 | |
209 | timeout_end_label.props.label = C_('option|greeter|screensaver-timeout', '{count} min').format(count=total) | |
210 | ||
211 | timeout_view.connect('format-value', self.on_entry_format_scale_greeter_screensaver_timeout, timeout_adjustment) | |
212 | entry.get.connect(self.on_entry_get_greeter_screensaver_timeout) | |
213 | entry.set.connect(self.on_entry_set_greeter_screensaver_timeout) | |
214 | ||
215 | def on_entry_setup_greeter_theme_name(self, entry): | |
216 | values = entry.widgets['values'] | |
217 | for theme in sorted(iglob(os.path.join(sys.prefix, 'share', 'themes', '*', 'gtk-3.0'))): | |
218 | values.append_text(theme.split(os.path.sep)[-2]) | |
219 | entry.changed.connect(self.on_entry_changed_greeter_theme_name) | |
220 | ||
221 | def on_entry_setup_greeter_icon_theme_name(self, entry): | |
222 | values = entry.widgets['values'] | |
223 | for theme in sorted(iglob(os.path.join(sys.prefix, 'share', 'icons', '*', 'index.theme'))): | |
224 | values.append_text(theme.split(os.path.sep)[-2]) | |
225 | entry.changed.connect(self.on_entry_changed_greeter_icon_theme_name) | |
226 | ||
227 | def on_entry_setup_greeter_default_user_image(self, entry): | |
228 | entry.changed.connect(self.on_entry_changed_greeter_default_user_image) | |
229 | ||
230 | def on_entry_setup_greeter_background(self, entry): | |
231 | entry.changed.connect(self.on_entry_changed_greeter_background) | |
232 | ||
233 | def on_entry_setup_greeter_allow_debugging(self, entry): | |
234 | if (Gtk.MAJOR_VERSION, Gtk.MINOR_VERSION, Gtk.MICRO_VERSION) < (3, 14, 0): | |
235 | entry.changed.connect(self.on_entry_changed_greeter_allow_debugging) | |
211 | view = entry.widgets['view'] | |
212 | adjustment = entry.widgets['adjustment'] | |
213 | end_label = entry.widgets['end-label'] | |
214 | for mark in chain((range(10, 61, 10)), | |
215 | (range(69, int(adjustment.props.upper), 10))): | |
216 | view.add_mark(mark, Gtk.PositionType.BOTTOM, None) | |
217 | total = int(adjustment.props.upper - 60) + 1 | |
218 | end_label.props.label = C_('option|greeter|screensaver-timeout', | |
219 | '{count} min').format(count=total) | |
220 | ||
221 | view.connect('format-value', | |
222 | self.on_entry_format_scale_greeter_screensaver_timeout, | |
223 | adjustment) | |
236 | 224 | |
237 | 225 | def on_entry_get_greeter_screensaver_timeout(self, entry=None, value=None): |
238 | 226 | value = int(float(value)) |
246 | 234 | return value // 60 + 59 |
247 | 235 | return value |
248 | 236 | |
249 | def on_entry_changed_greeter_theme_name(self, entry): | |
250 | if not entry.value or entry.value in (row[0] for row in entry.widgets['values'].props.model): | |
251 | entry.error = None | |
252 | else: | |
253 | entry.error = C_('option|greeter|theme-name', 'Selected theme is not available') | |
254 | ||
255 | def on_entry_changed_greeter_icon_theme_name(self, entry): | |
256 | if not entry.value or entry.value in (row[0] for row in entry.widgets['values'].props.model): | |
257 | entry.error = None | |
258 | else: | |
259 | entry.error = C_('option|greeter|icon-theme-name', 'Selected theme is not available') | |
260 | ||
261 | def on_entry_changed_greeter_default_user_image(self, entry): | |
262 | value = entry.value | |
263 | if value.startswith('#'): | |
264 | entry.error = None | |
265 | elif not os.path.exists(value): | |
266 | entry.error = C_('option|greeter|default-user-image', 'File not found: {path}'.format(path=value)) | |
267 | else: | |
268 | try: | |
269 | if not helpers.file_is_readable_by_greeter(value): | |
270 | entry.error = C_('option|greeter|default-user-image', 'File may be not readable for greeter: {path}'.format(path=value)) | |
271 | else: | |
272 | entry.error = None | |
273 | except: | |
274 | entry.error = C_('option|greeter|default-user-image', 'Failed to check permissions for file: {path}'.format(path=value)) | |
275 | ||
276 | def on_entry_changed_greeter_background(self, entry): | |
277 | value = entry.value | |
278 | if Gdk.RGBA().parse(value): | |
279 | entry.error = None | |
280 | elif not os.path.exists(value): | |
281 | entry.error = C_('option|greeter|background', 'File not found: {path}'.format(path=value)) | |
282 | else: | |
283 | try: | |
284 | if not helpers.file_is_readable_by_greeter(value): | |
285 | entry.error = C_('option|greeter|background', 'File may be not readable for greeter: {path}'.format(path=value)) | |
286 | else: | |
287 | entry.error = None | |
288 | except: | |
289 | entry.error = C_('option|greeter|background', 'Failed to check permissions for file: {path}'.format(path=value)) | |
290 | ||
291 | def on_entry_changed_greeter_allow_debugging(self, entry): | |
292 | if string2bool(entry.value): | |
293 | entry.error = C_('option|greeter|allow-debugging', | |
294 | 'GtkInspector is not available on your system') | |
295 | else: | |
296 | entry.error = None | |
297 | ||
298 | 237 | def on_entry_format_scale_greeter_screensaver_timeout(self, scale, value, adjustment): |
299 | 238 | if value != adjustment.props.lower and value != adjustment.props.upper: |
300 | 239 | value = self.on_entry_get_greeter_screensaver_timeout(value=value) |
301 | 240 | return '%02d:%02d' % (value // 60, value % 60) |
302 | else: | |
303 | return '' | |
241 | return '' | |
242 | ||
243 | # [greeter] theme-name | |
244 | def on_entry_setup_greeter_theme_name(self, entry): | |
245 | values = entry.widgets['values'] | |
246 | for theme in sorted(iglob(os.path.join(sys.prefix, 'share', 'themes', '*', 'gtk-3.0'))): | |
247 | values.append_text(theme.split(os.path.sep)[-2]) | |
248 | ||
249 | def on_entry_changed_greeter_theme_name(self, entry): | |
250 | if not entry.value or \ | |
251 | entry.value in (row[0] for row in entry.widgets['values'].props.model): | |
252 | entry.error = None | |
253 | else: | |
254 | entry.error = C_('option|greeter|theme-name', 'Selected theme is not available') | |
255 | ||
256 | # [greeter] icon-theme-name | |
257 | def on_entry_setup_greeter_icon_theme_name(self, entry): | |
258 | values = entry.widgets['values'] | |
259 | for theme in sorted(iglob(os.path.join(sys.prefix, 'share', 'icons', '*', 'index.theme'))): | |
260 | values.append_text(theme.split(os.path.sep)[-2]) | |
261 | ||
262 | def on_entry_changed_greeter_icon_theme_name(self, entry): | |
263 | if not entry.value or \ | |
264 | entry.value in (row[0] for row in entry.widgets['values'].props.model): | |
265 | entry.error = None | |
266 | else: | |
267 | entry.error = C_('option|greeter|icon-theme-name', 'Selected theme is not available') | |
268 | ||
269 | # [greeter] allow-debugging | |
270 | def on_entry_changed_greeter_allow_debugging(self, entry): | |
271 | if (Gtk.MAJOR_VERSION, Gtk.MINOR_VERSION, Gtk.MICRO_VERSION) < (3, 14, 0) and \ | |
272 | string2bool(entry.value): | |
273 | entry.error = C_('option|greeter|allow-debugging', | |
274 | 'GtkInspector is not available on your system') | |
275 | else: | |
276 | entry.error = None | |
277 | ||
278 | # [greeter] default-user-image | |
279 | def on_entry_changed_greeter_default_user_image(self, entry): | |
280 | value = entry.value | |
281 | if value.startswith('#'): | |
282 | entry.error = None | |
283 | else: | |
284 | entry.error = helpers.check_path_accessibility(value) | |
285 | ||
286 | # [greeter] background | |
287 | def on_entry_changed_greeter_background(self, entry): | |
288 | value = entry.value | |
289 | if not value or Gdk.RGBA().parse(value): | |
290 | entry.error = None | |
291 | else: | |
292 | entry.error = helpers.check_path_accessibility(value) | |
304 | 293 | |
305 | 294 | def on_destroy(self, *unused): |
306 | 295 | Gtk.main_quit() |
14 | 14 | # You should have received a copy of the GNU General Public License along |
15 | 15 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 | from collections import namedtuple | |
18 | from gi.repository import Gtk, GObject | |
19 | from lightdm_gtk_greeter_settings.helpers import ModelRowEnum | |
20 | from lightdm_gtk_greeter_settings.helpers import get_data_path | |
21 | from lightdm_gtk_greeter_settings.helpers import NC_, C_ | |
17 | ||
18 | from gi.repository import ( | |
19 | GObject, | |
20 | Gtk) | |
21 | ||
22 | from lightdm_gtk_greeter_settings.helpers import ( | |
23 | get_data_path, | |
24 | NC_, | |
25 | SimpleEnum, | |
26 | WidgetsEnum) | |
22 | 27 | |
23 | 28 | |
24 | 29 | __all__ = ['IconChooserDialog'] |
25 | 30 | |
26 | CONTEXT_ROW = ModelRowEnum('NAME', 'STANDARD', 'TITLE') | |
27 | ICON_ROW = ModelRowEnum('NAME', 'STANDARD', 'CONTEXT') | |
31 | ||
32 | class ContextRow(SimpleEnum): | |
33 | Name = () | |
34 | Standard = () | |
35 | Title = () | |
36 | ||
37 | ||
38 | class IconRow(SimpleEnum): | |
39 | Name = () | |
40 | Standard = () | |
41 | Context = () | |
28 | 42 | |
29 | 43 | |
30 | 44 | class IconChooserDialog(Gtk.Dialog): |
31 | 45 | |
32 | 46 | __gtype_name__ = 'IconChooserDialog' |
33 | ||
34 | IconsFilterArgs = namedtuple('IconsFilterArgs', ('standard', 'context')) | |
35 | ||
36 | BUILDER_WIDGETS = ('name', 'preview', 'standard_toggle', 'spinner', | |
37 | 'contexts_view', 'contexts_selection', 'contexts_model', | |
38 | 'contexts_filter', 'icons_view', 'icons_selection', | |
39 | 'icons_model', 'icons_sorted', 'icons_filter') | |
40 | 47 | |
41 | 48 | def __new__(cls): |
42 | 49 | builder = Gtk.Builder() |
43 | 50 | builder.add_from_file(get_data_path('%s.ui' % cls.__name__)) |
44 | 51 | window = builder.get_object('icon_chooser_dialog') |
45 | window._builder = builder | |
46 | window.__dict__.update(('_' + w, builder.get_object(w)) | |
47 | for w in cls.BUILDER_WIDGETS) | |
48 | ||
52 | window.builder = builder | |
49 | 53 | builder.connect_signals(window) |
50 | window._init_window() | |
54 | window.init_window() | |
51 | 55 | return window |
52 | 56 | |
53 | def _init_window(self): | |
57 | class Widgets(WidgetsEnum): | |
58 | name = 'name_entry' | |
59 | preview = 'preview_image' | |
60 | standard = 'standard_toggle' | |
61 | ok = 'ok_button' | |
62 | cancel = 'cancel_button' | |
63 | contexts_view = 'contexts_view' | |
64 | contexts_selection = 'contexts_selection' | |
65 | contexts_model = 'contexts_model' | |
66 | contexts_filter = 'contexts_filter' | |
67 | icons_view = 'icons_view' | |
68 | icons_selection = 'icons_selection' | |
69 | icons_model = 'icons_model' | |
70 | icons_sorted = 'icons_sorted' | |
71 | icons_filter = 'icons_filter' | |
72 | ||
73 | builder = None | |
74 | ||
75 | def init_window(self): | |
76 | self._widgets = self.Widgets(builder=self.builder) | |
77 | self._widgets_to_disable = (self._widgets.name, self._widgets.standard, | |
78 | self._widgets.contexts_view, self._widgets.icons_view, | |
79 | self._widgets.ok, self._widgets.cancel) | |
54 | 80 | |
55 | 81 | self._icons_loaded = False |
56 | 82 | self._icon_to_select = None |
57 | self._icons_filter_args = None | |
58 | ||
59 | self._contexts_view.set_row_separator_func( | |
60 | self._contexts_row_separator_callback, None) | |
61 | self._contexts_filter.set_visible_func( | |
62 | self._contexts_filter_visible_callback) | |
63 | ||
64 | self._icons_filter.set_visible_func( | |
65 | self._icons_filter_visible_callback) | |
66 | self._icons_sorted.set_sort_column_id(0, Gtk.SortType.ASCENDING) | |
83 | self._icon_filter_standard = True | |
84 | self._icon_filter_context = None | |
85 | ||
86 | self._widgets.contexts_view.set_row_separator_func(self._contexts_separator_callback, None) | |
87 | self._widgets.contexts_filter.set_visible_func(self._contexts_visible_callback) | |
88 | ||
89 | self._widgets.icons_filter.set_visible_func(self._icons_filter_visible_callback) | |
90 | self._widgets.icons_sorted.set_sort_column_id(0, Gtk.SortType.ASCENDING) | |
67 | 91 | |
68 | 92 | self._reload() |
93 | ||
94 | def _reload(self): | |
95 | for w in self._widgets_to_disable: | |
96 | w.props.sensitive = False | |
97 | GObject.idle_add(self._read_icons) | |
69 | 98 | |
70 | 99 | def _read_icons(self): |
71 | 100 | theme = Gtk.IconTheme.get_default() |
72 | standard_contexts = set(name for name, title in STANDARD_CONTEXTS) | |
73 | ||
74 | self._contexts_model.clear() | |
75 | for name, title in STANDARD_CONTEXTS: | |
76 | row = CONTEXT_ROW(NAME=name, STANDARD=True, | |
77 | TITLE=title and C_('icon-dialog', title)) | |
78 | self._contexts_model.append(row) | |
101 | standard_contexts = set(name for name, title in StandardContexts) | |
102 | ||
103 | self._widgets.contexts_model.clear() | |
104 | for name, title in StandardContexts: | |
105 | row = ContextRow._make(Name=name, Standard=True, Title=title) | |
106 | self._widgets.contexts_model.append(row) | |
79 | 107 | |
80 | 108 | for name in theme.list_contexts(): |
81 | 109 | if name not in standard_contexts: |
82 | row = CONTEXT_ROW(NAME=name, STANDARD=False, TITLE=name) | |
83 | self._contexts_model.append(row) | |
84 | ||
85 | self._icons_model.clear() | |
110 | row = ContextRow._make(Name=name, Standard=False, Title=name) | |
111 | self._widgets.contexts_model.append(row) | |
112 | ||
113 | self._widgets.icons_model.clear() | |
86 | 114 | for context in theme.list_contexts(): |
87 | 115 | for icon in theme.list_icons(context): |
88 | row = ICON_ROW(NAME=icon, CONTEXT=context, | |
89 | STANDARD=icon in STANDARD_ICON_NAMES) | |
90 | self._icons_model.append(row) | |
116 | row = IconRow._make(Name=icon, Standard=icon in StandardIconNames, Context=context) | |
117 | self._widgets.icons_model.append(row) | |
91 | 118 | |
92 | 119 | self._icons_loaded = True |
93 | 120 | if self._icon_to_select: |
94 | 121 | self.select_icon(self._icon_to_select) |
95 | 122 | self._icon_to_select = None |
123 | ||
124 | for w in self._widgets_to_disable: | |
125 | w.props.sensitive = True | |
126 | ||
96 | 127 | return False |
97 | 128 | |
98 | def _reload(self): | |
99 | GObject.idle_add(self._read_icons) | |
100 | ||
101 | 129 | def _update_contexts_filter(self): |
102 | selected_iter = self._contexts_selection.get_selected()[1] | |
103 | selected_path = self._contexts_filter.get_path( | |
104 | selected_iter) if selected_iter else None | |
105 | self._contexts_filter.refilter() | |
106 | if selected_path and \ | |
107 | self._contexts_selection.path_is_selected(selected_path): | |
108 | self._update_icons_filter() | |
130 | self._widgets.contexts_filter.refilter() | |
131 | self._update_icons_filter() | |
109 | 132 | |
110 | 133 | def _update_icons_filter(self): |
111 | model, rowiter = self._contexts_selection.get_selected() | |
134 | self._widgets.icons_view.props.model = None | |
135 | model, rowiter = self._widgets.contexts_selection.get_selected() | |
112 | 136 | if rowiter: |
113 | self._icons_filter_args = self.IconsFilterArgs( | |
114 | self._standard_toggle.props.active, | |
115 | model[rowiter][CONTEXT_ROW.NAME]) | |
116 | else: | |
117 | self._icons_filter_args = None | |
118 | self._icons_view.props.model = None | |
119 | self._icons_filter.refilter() | |
120 | self._icons_view.props.model = self._icons_sorted | |
121 | ||
122 | def _contexts_filter_visible_callback(self, model, rowiter, data): | |
123 | if not self._standard_toggle.props.active: | |
137 | self._icon_filter_standard = self._widgets.standard.props.active | |
138 | self._icon_filter_context = model[rowiter][ContextRow.Name] | |
139 | self._widgets.icons_filter.refilter() | |
140 | self._widgets.icons_view.props.model = self._widgets.icons_sorted | |
141 | ||
142 | def _contexts_visible_callback(self, model, rowiter, data): | |
143 | if not self._widgets.standard.props.active: | |
124 | 144 | return True |
125 | return model[rowiter][CONTEXT_ROW.STANDARD] | |
126 | ||
127 | def _contexts_row_separator_callback(self, model, rowiter, data): | |
128 | return not model[rowiter][CONTEXT_ROW.NAME] and \ | |
129 | not model[rowiter][CONTEXT_ROW.TITLE] | |
145 | return model[rowiter][ContextRow.Standard] | |
146 | ||
147 | def _contexts_separator_callback(self, model, rowiter, data): | |
148 | return not model[rowiter][ContextRow.Name] and not model[rowiter][ContextRow.Title] | |
130 | 149 | |
131 | 150 | def _icons_filter_visible_callback(self, model, rowiter, data): |
132 | if not self._icons_filter_args: | |
151 | if self._icon_filter_standard and not model[rowiter][IconRow.Standard]: | |
133 | 152 | return False |
134 | if self._icons_filter_args.standard and \ | |
135 | not model[rowiter][ICON_ROW.STANDARD]: | |
136 | return False | |
137 | if not self._icons_filter_args.context: | |
153 | if not self._icon_filter_context: | |
138 | 154 | return True |
139 | return model[rowiter][ICON_ROW.CONTEXT] == \ | |
140 | self._icons_filter_args.context | |
141 | ||
142 | def run(self): | |
143 | return super().run() | |
144 | ||
145 | def get_icon_name(self): | |
146 | return self._name.props.text | |
155 | return model[rowiter][IconRow.Context] == self._icon_filter_context | |
156 | ||
157 | def get_selected_icon(self): | |
158 | return self._widgets.name.props.text | |
147 | 159 | |
148 | 160 | def select_icon(self, name): |
149 | 161 | if not self._icons_loaded: |
150 | 162 | self._icon_to_select = name |
151 | 163 | return |
152 | 164 | |
153 | if not self._icons_filter_args or \ | |
154 | self._icons_filter_args.context is not None: | |
155 | if name not in STANDARD_ICON_NAMES: | |
156 | self._standard_toggle.props.active = False | |
157 | self._contexts_selection.select_path(0) | |
158 | for row in self._icons_sorted: | |
159 | if row[ICON_ROW.NAME] == name: | |
160 | self._icons_view.set_cursor(row.path) | |
161 | self._icons_selection.select_path(row.path) | |
165 | if name not in StandardIconNames: | |
166 | self._widgets.standard.props.active = False | |
167 | self._widgets.contexts_selection.select_path(0) | |
168 | ||
169 | for row in self._widgets.icons_sorted: | |
170 | if row[IconRow.Name] == name: | |
171 | self._widgets.icons_view.set_cursor(row.path) | |
172 | self._widgets.icons_selection.select_path(row.path) | |
162 | 173 | break |
163 | 174 | else: |
164 | self._name.props.text = name | |
175 | self._widgets.name.props.text = name | |
165 | 176 | |
166 | 177 | def on_icons_selection_changed(self, selection): |
167 | model, rowiter = self._icons_selection.get_selected() | |
178 | model, rowiter = selection.get_selected() | |
168 | 179 | if rowiter: |
169 | name = model[rowiter][ICON_ROW.NAME] | |
170 | self._name.props.text = name | |
180 | self._widgets.name.props.text = model[rowiter][IconRow.Name] | |
171 | 181 | |
172 | 182 | def on_contexts_selection_changed(self, selection): |
173 | self._icons_selection.unselect_all() | |
183 | self._widgets.icons_selection.unselect_all() | |
174 | 184 | self._update_icons_filter() |
175 | 185 | |
176 | 186 | def on_standard_toggled(self, toggle): |
180 | 190 | name = entry.props.text |
181 | 191 | if not Gtk.IconTheme.get_default().has_icon(name): |
182 | 192 | name = '' |
183 | self._preview.props.icon_name = name | |
193 | self._widgets.preview.props.icon_name = name | |
184 | 194 | |
185 | 195 | |
186 | 196 | # http://standards.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html |
187 | 197 | |
188 | STANDARD_CONTEXTS =\ | |
189 | ( | |
190 | (None, NC_('icon-dialog', 'All contexts')), | |
191 | (None, ''), # separator | |
192 | ('Actions', NC_('icon-dialog', 'Actions')), | |
193 | ('Applications', NC_('icon-dialog', 'Applications')), | |
194 | ('Categories', NC_('icon-dialog', 'Categories')), | |
195 | ('Devices', NC_('icon-dialog', 'Devices')), | |
196 | ('Emblems', NC_('icon-dialog', 'Emblems')), | |
197 | ('Emotes', NC_('icon-dialog', 'Emoticons')), | |
198 | ('International', NC_('icon-dialog', 'International')), | |
199 | ('MimeTypes', NC_('icon-dialog', 'MIME Types')), | |
200 | ('Places', NC_('icon-dialog', 'Places')), | |
201 | ('Status', NC_('icon-dialog', 'Status')) | |
202 | ) | |
203 | ||
204 | STANDARD_ICON_NAMES = \ | |
205 | { | |
206 | # Actions | |
207 | 'address-book-new', 'application-exit', 'appointment-new', | |
208 | 'call-start', 'call-stop', 'contact-new', 'document-new', | |
209 | 'document-open', 'document-open-recent', 'document-page-setup', | |
210 | 'document-print', 'document-print-preview', 'document-properties', | |
211 | 'document-revert', 'document-save', 'document-save-as', | |
212 | 'document-send', 'edit-clear', 'edit-copy', 'edit-cut', 'edit-delete', | |
213 | 'edit-find', 'edit-find-replace', 'edit-paste', 'edit-redo', | |
214 | 'edit-select-all', 'edit-undo', 'folder-new', 'format-indent-less', | |
215 | 'format-indent-more', 'format-justify-center', 'format-justify-fill', | |
216 | 'format-justify-left', 'format-justify-right', | |
217 | 'format-text-direction-ltr', 'format-text-direction-rtl', | |
218 | 'format-text-bold', 'format-text-italic', 'format-text-underline', | |
219 | 'format-text-strikethrough', 'go-bottom', 'go-down', 'go-first', | |
220 | 'go-home', 'go-jump', 'go-last', 'go-next', 'go-previous', 'go-top', | |
221 | 'go-up', 'help-about', 'help-contents', 'help-faq', 'insert-image', | |
222 | 'insert-link', 'insert-object', 'insert-text', 'list-add', | |
223 | 'list-remove', 'mail-forward', 'mail-mark-important', 'mail-mark-junk', | |
224 | 'mail-mark-notjunk', 'mail-mark-read', 'mail-mark-unread', | |
225 | 'mail-message-new', 'mail-reply-all', 'mail-reply-sender', 'mail-send', | |
226 | 'mail-send-receive', 'media-eject', 'media-playback-pause', | |
227 | 'media-playback-start', 'media-playback-stop', 'media-record', | |
228 | 'media-seek-backward', 'media-seek-forward', 'media-skip-backward', | |
229 | 'media-skip-forward', 'object-flip-horizontal', 'object-flip-vertical', | |
230 | 'object-rotate-left', 'object-rotate-right', 'process-stop', | |
231 | 'system-lock-screen', 'system-log-out', 'system-run', 'system-search', | |
232 | 'system-reboot', 'system-shutdown', 'tools-check-spelling', | |
233 | 'view-fullscreen', 'view-refresh', 'view-restore', | |
234 | 'view-sort-ascending', 'view-sort-descending', 'window-close', | |
235 | 'window-new', 'zoom-fit-best', 'zoom-in', 'zoom-original', 'zoom-out', | |
236 | # StandardApplicationIcons | |
237 | 'accessories-calculator', 'accessories-character-map', | |
238 | 'accessories-dictionary', 'accessories-text-editor', 'help-browser', | |
239 | 'multimedia-volume-control', 'preferences-desktop-accessibility', | |
240 | 'preferences-desktop-font', 'preferences-desktop-keyboard', | |
241 | 'preferences-desktop-locale', 'preferences-desktop-multimedia', | |
242 | 'preferences-desktop-screensaver', 'preferences-desktop-theme', | |
243 | 'preferences-desktop-wallpaper', 'system-file-manager', | |
244 | 'system-software-install', 'system-software-update', | |
245 | 'utilities-system-monitor', 'utilities-terminal', | |
246 | # StandardCategoryIcons | |
247 | 'applications-accessories', 'applications-development', | |
248 | 'applications-engineering', 'applications-games', | |
249 | 'applications-graphics', 'applications-internet', | |
250 | 'applications-multimedia', 'applications-office', 'applications-other', | |
251 | 'applications-science', 'applications-system', | |
252 | 'applications-utilities', 'preferences-desktop', | |
253 | 'preferences-desktop-peripherals', 'preferences-desktop-personal', | |
254 | 'preferences-other', 'preferences-system', | |
255 | 'preferences-system-network', 'system-help', | |
256 | # StandardDeviceIcons | |
257 | 'audio-card', 'audio-input-microphone', 'battery', 'camera-photo', | |
258 | 'camera-video', 'camera-web', 'computer', 'drive-harddisk', | |
259 | 'drive-optical', 'drive-removable-media', 'input-gaming', | |
260 | 'input-keyboard', 'input-mouse', 'input-tablet', 'media-flash', | |
261 | 'media-floppy', 'media-optical', 'media-tape', 'modem', | |
262 | 'multimedia-player', 'network-wired', 'network-wireless', 'pda', | |
263 | 'phone', 'printer', 'scanner', 'video-display', | |
264 | # StandardEmblemIcons | |
265 | 'emblem-default', 'emblem-documents', 'emblem-downloads', | |
266 | 'emblem-favorite', 'emblem-important', 'emblem-mail', 'emblem-photos', | |
267 | 'emblem-readonly', 'emblem-shared', 'emblem-symbolic-link', | |
268 | 'emblem-synchronized', 'emblem-system', 'emblem-unreadable', | |
269 | # StandardEmotionIcons | |
270 | 'face-angel', 'face-angry', 'face-cool', 'face-crying', | |
271 | 'face-devilish', 'face-embarrassed', 'face-kiss', 'face-laugh', | |
272 | 'face-monkey', 'face-plain', 'face-raspberry', 'face-sad', 'face-sick', | |
273 | 'face-smile', 'face-smile-big', 'face-smirk', 'face-surprise', | |
274 | 'face-tired', 'face-uncertain', 'face-wink', 'face-worried', | |
275 | # StandardInternationalIcons | |
276 | 'flag-aa', | |
277 | # StandardMIMETypeIcons | |
278 | 'application-x-executable', 'audio-x-generic', 'font-x-generic', | |
279 | 'image-x-generic', 'package-x-generic', 'text-html', 'text-x-generic', | |
280 | 'text-x-generic-template', 'text-x-script', 'video-x-generic', | |
281 | 'x-office-address-book', 'x-office-calendar', 'x-office-document', | |
282 | 'x-office-presentation', 'x-office-spreadsheet', | |
283 | # StandardPlaceIcons | |
284 | 'folder', 'folder-remote', 'network-server', 'network-workgroup', | |
285 | 'start-here', 'user-bookmarks', 'user-desktop', 'user-home', | |
286 | 'user-trash', | |
287 | # StandardStatusIcons | |
288 | 'appointment-missed', 'appointment-soon', 'audio-volume-high', | |
289 | 'audio-volume-low', 'audio-volume-medium', 'audio-volume-muted', | |
290 | 'battery-caution', 'battery-low', 'dialog-error', 'dialog-information', | |
291 | 'dialog-password', 'dialog-question', 'dialog-warning', | |
292 | 'folder-drag-accept', 'folder-open', 'folder-visiting', | |
293 | 'image-loading', 'image-missing', 'mail-attachment', 'mail-unread', | |
294 | 'mail-read', 'mail-replied', 'mail-signed', 'mail-signed-verified', | |
295 | 'media-playlist-repeat', 'media-playlist-shuffle', 'network-error', | |
296 | 'network-idle', 'network-offline', 'network-receive', | |
297 | 'network-transmit', 'network-transmit-receive', 'printer-error', | |
298 | 'printer-printing', 'security-high', 'security-medium', 'security-low', | |
299 | 'software-update-available', 'software-update-urgent', 'sync-error', | |
300 | 'sync-synchronizing', 'task-due', 'task-past-due', 'user-available', | |
301 | 'user-away', 'user-idle', 'user-offline', 'user-trash-full', | |
302 | 'weather-clear', 'weather-clear-night', 'weather-few-clouds', | |
303 | 'weather-few-clouds-night', 'weather-fog', 'weather-overcast', | |
304 | 'weather-severe-alert', 'weather-showers', 'weather-showers-scattered', | |
305 | 'weather-snow', 'weather-storm' | |
306 | } | |
198 | StandardContexts = ( | |
199 | (None, NC_('icon-dialog', 'All contexts')), | |
200 | (None, ''), # separator | |
201 | ('Actions', NC_('icon-dialog', 'Actions')), | |
202 | ('Applications', NC_('icon-dialog', 'Applications')), | |
203 | ('Categories', NC_('icon-dialog', 'Categories')), | |
204 | ('Devices', NC_('icon-dialog', 'Devices')), | |
205 | ('Emblems', NC_('icon-dialog', 'Emblems')), | |
206 | ('Emotes', NC_('icon-dialog', 'Emoticons')), | |
207 | ('International', NC_('icon-dialog', 'International')), | |
208 | ('MimeTypes', NC_('icon-dialog', 'MIME Types')), | |
209 | ('Places', NC_('icon-dialog', 'Places')), | |
210 | ('Status', NC_('icon-dialog', 'Status'))) | |
211 | ||
212 | StandardIconNames = { | |
213 | # Actions | |
214 | 'address-book-new', 'application-exit', 'appointment-new', | |
215 | 'call-start', 'call-stop', 'contact-new', 'document-new', | |
216 | 'document-open', 'document-open-recent', 'document-page-setup', | |
217 | 'document-print', 'document-print-preview', 'document-properties', | |
218 | 'document-revert', 'document-save', 'document-save-as', | |
219 | 'document-send', 'edit-clear', 'edit-copy', 'edit-cut', 'edit-delete', | |
220 | 'edit-find', 'edit-find-replace', 'edit-paste', 'edit-redo', | |
221 | 'edit-select-all', 'edit-undo', 'folder-new', 'format-indent-less', | |
222 | 'format-indent-more', 'format-justify-center', 'format-justify-fill', | |
223 | 'format-justify-left', 'format-justify-right', | |
224 | 'format-text-direction-ltr', 'format-text-direction-rtl', | |
225 | 'format-text-bold', 'format-text-italic', 'format-text-underline', | |
226 | 'format-text-strikethrough', 'go-bottom', 'go-down', 'go-first', | |
227 | 'go-home', 'go-jump', 'go-last', 'go-next', 'go-previous', 'go-top', | |
228 | 'go-up', 'help-about', 'help-contents', 'help-faq', 'insert-image', | |
229 | 'insert-link', 'insert-object', 'insert-text', 'list-add', | |
230 | 'list-remove', 'mail-forward', 'mail-mark-important', 'mail-mark-junk', | |
231 | 'mail-mark-notjunk', 'mail-mark-read', 'mail-mark-unread', | |
232 | 'mail-message-new', 'mail-reply-all', 'mail-reply-sender', 'mail-send', | |
233 | 'mail-send-receive', 'media-eject', 'media-playback-pause', | |
234 | 'media-playback-start', 'media-playback-stop', 'media-record', | |
235 | 'media-seek-backward', 'media-seek-forward', 'media-skip-backward', | |
236 | 'media-skip-forward', 'object-flip-horizontal', 'object-flip-vertical', | |
237 | 'object-rotate-left', 'object-rotate-right', 'process-stop', | |
238 | 'system-lock-screen', 'system-log-out', 'system-run', 'system-search', | |
239 | 'system-reboot', 'system-shutdown', 'tools-check-spelling', | |
240 | 'view-fullscreen', 'view-refresh', 'view-restore', | |
241 | 'view-sort-ascending', 'view-sort-descending', 'window-close', | |
242 | 'window-new', 'zoom-fit-best', 'zoom-in', 'zoom-original', 'zoom-out', | |
243 | # StandardApplicationIcons | |
244 | 'accessories-calculator', 'accessories-character-map', | |
245 | 'accessories-dictionary', 'accessories-text-editor', 'help-browser', | |
246 | 'multimedia-volume-control', 'preferences-desktop-accessibility', | |
247 | 'preferences-desktop-font', 'preferences-desktop-keyboard', | |
248 | 'preferences-desktop-locale', 'preferences-desktop-multimedia', | |
249 | 'preferences-desktop-screensaver', 'preferences-desktop-theme', | |
250 | 'preferences-desktop-wallpaper', 'system-file-manager', | |
251 | 'system-software-install', 'system-software-update', | |
252 | 'utilities-system-monitor', 'utilities-terminal', | |
253 | # StandardCategoryIcons | |
254 | 'applications-accessories', 'applications-development', | |
255 | 'applications-engineering', 'applications-games', | |
256 | 'applications-graphics', 'applications-internet', | |
257 | 'applications-multimedia', 'applications-office', 'applications-other', | |
258 | 'applications-science', 'applications-system', | |
259 | 'applications-utilities', 'preferences-desktop', | |
260 | 'preferences-desktop-peripherals', 'preferences-desktop-personal', | |
261 | 'preferences-other', 'preferences-system', | |
262 | 'preferences-system-network', 'system-help', | |
263 | # StandardDeviceIcons | |
264 | 'audio-card', 'audio-input-microphone', 'battery', 'camera-photo', | |
265 | 'camera-video', 'camera-web', 'computer', 'drive-harddisk', | |
266 | 'drive-optical', 'drive-removable-media', 'input-gaming', | |
267 | 'input-keyboard', 'input-mouse', 'input-tablet', 'media-flash', | |
268 | 'media-floppy', 'media-optical', 'media-tape', 'modem', | |
269 | 'multimedia-player', 'network-wired', 'network-wireless', 'pda', | |
270 | 'phone', 'printer', 'scanner', 'video-display', | |
271 | # StandardEmblemIcons | |
272 | 'emblem-default', 'emblem-documents', 'emblem-downloads', | |
273 | 'emblem-favorite', 'emblem-important', 'emblem-mail', 'emblem-photos', | |
274 | 'emblem-readonly', 'emblem-shared', 'emblem-symbolic-link', | |
275 | 'emblem-synchronized', 'emblem-system', 'emblem-unreadable', | |
276 | # StandardEmotionIcons | |
277 | 'face-angel', 'face-angry', 'face-cool', 'face-crying', | |
278 | 'face-devilish', 'face-embarrassed', 'face-kiss', 'face-laugh', | |
279 | 'face-monkey', 'face-plain', 'face-raspberry', 'face-sad', 'face-sick', | |
280 | 'face-smile', 'face-smile-big', 'face-smirk', 'face-surprise', | |
281 | 'face-tired', 'face-uncertain', 'face-wink', 'face-worried', | |
282 | # StandardInternationalIcons | |
283 | 'flag-aa', | |
284 | # StandardMIMETypeIcons | |
285 | 'application-x-executable', 'audio-x-generic', 'font-x-generic', | |
286 | 'image-x-generic', 'package-x-generic', 'text-html', 'text-x-generic', | |
287 | 'text-x-generic-template', 'text-x-script', 'video-x-generic', | |
288 | 'x-office-address-book', 'x-office-calendar', 'x-office-document', | |
289 | 'x-office-presentation', 'x-office-spreadsheet', | |
290 | # StandardPlaceIcons | |
291 | 'folder', 'folder-remote', 'network-server', 'network-workgroup', | |
292 | 'start-here', 'user-bookmarks', 'user-desktop', 'user-home', | |
293 | 'user-trash', | |
294 | # StandardStatusIcons | |
295 | 'appointment-missed', 'appointment-soon', 'audio-volume-high', | |
296 | 'audio-volume-low', 'audio-volume-medium', 'audio-volume-muted', | |
297 | 'battery-caution', 'battery-low', 'dialog-error', 'dialog-information', | |
298 | 'dialog-password', 'dialog-question', 'dialog-warning', | |
299 | 'folder-drag-accept', 'folder-open', 'folder-visiting', | |
300 | 'image-loading', 'image-missing', 'mail-attachment', 'mail-unread', | |
301 | 'mail-read', 'mail-replied', 'mail-signed', 'mail-signed-verified', | |
302 | 'media-playlist-repeat', 'media-playlist-shuffle', 'network-error', | |
303 | 'network-idle', 'network-offline', 'network-receive', | |
304 | 'network-transmit', 'network-transmit-receive', 'printer-error', | |
305 | 'printer-printing', 'security-high', 'security-medium', 'security-low', | |
306 | 'software-update-available', 'software-update-urgent', 'sync-error', | |
307 | 'sync-synchronizing', 'task-due', 'task-past-due', 'user-available', | |
308 | 'user-away', 'user-idle', 'user-offline', 'user-trash-full', | |
309 | 'weather-clear', 'weather-clear-night', 'weather-few-clouds', | |
310 | 'weather-few-clouds-night', 'weather-fog', 'weather-overcast', | |
311 | 'weather-severe-alert', 'weather-showers', 'weather-showers-scattered', | |
312 | 'weather-snow', 'weather-storm'} |
0 | #!/usr/bin/env python3 | |
1 | # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- | |
2 | # LightDM GTK Greeter Settings | |
3 | # Copyright (C) 2014 Andrew P. <pan.pav.7c5@gmail.com> | |
4 | # | |
5 | # This program is free software: you can redistribute it and/or modify it | |
6 | # under the terms of the GNU General Public License version 3, as published | |
7 | # by the Free Software Foundation. | |
8 | # | |
9 | # This program is distributed in the hope that it will be useful, but | |
10 | # WITHOUT ANY WARRANTY; without even the implied warranties of | |
11 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | |
12 | # PURPOSE. See the GNU General Public License for more details. | |
13 | # | |
14 | # You should have received a copy of the GNU General Public License along | |
15 | # with this program. If not, see <http://www.gnu.org/licenses/>. | |
16 | ||
17 | from glob import iglob | |
18 | import os | |
19 | import sys | |
20 | ||
21 | from gi.repository import Gtk | |
22 | from lightdm_gtk_greeter_settings import helpers | |
23 | ||
24 | ||
25 | __all__ = ['IndicatorChooserDialog'] | |
26 | ||
27 | ||
28 | class IndicatorChooserDialog(Gtk.Dialog): | |
29 | ||
30 | __gtype_name__ = 'IndicatorChooserDialog' | |
31 | ||
32 | BUILDER_WIDGETS = ('short_choice', 'short_value', 'short_model', | |
33 | 'path_choice', 'path_value', | |
34 | 'spacer_choice', 'separator_choice', | |
35 | 'add_button', 'ok_button', 'infobar', 'message') | |
36 | ||
37 | def __new__(cls, check_callback=None, add_callback=None): | |
38 | builder = Gtk.Builder() | |
39 | builder.add_from_file(helpers.get_data_path('%s.ui' % cls.__name__)) | |
40 | window = builder.get_object('indicator_chooser_dialog') | |
41 | window._builder = builder | |
42 | window.__dict__.update(('_' + w, builder.get_object(w)) | |
43 | for w in cls.BUILDER_WIDGETS) | |
44 | ||
45 | builder.connect_signals(window) | |
46 | window._init_window(check_callback, add_callback) | |
47 | return window | |
48 | ||
49 | def _init_window(self, check_callback, add_callback): | |
50 | self._check_callback = check_callback | |
51 | self._add_callback = add_callback | |
52 | self._add_button.props.visible = add_callback is not None | |
53 | ||
54 | for path in sorted(iglob(os.path.join(sys.prefix, 'share', 'unity', | |
55 | 'indicators', '*'))): | |
56 | name = os.path.basename(path) | |
57 | parts = name.rsplit('.', maxsplit=1) | |
58 | if len(parts) == 2 and parts[0] == 'com.canonical.indicator': | |
59 | name = parts[1] | |
60 | self._short_model.append((name,)) | |
61 | ||
62 | for path in sorted(iglob(os.path.join(sys.prefix, 'lib', 'indicators3', | |
63 | '7', '*.so'))): | |
64 | self._short_model.append((os.path.basename(path),)) | |
65 | ||
66 | def _get_current_value(self): | |
67 | if self._short_choice.props.active: | |
68 | return self._short_value.props.text | |
69 | elif self._path_choice.props.active: | |
70 | return self._path_value.get_filename() | |
71 | elif self._spacer_choice.props.active: | |
72 | return '~spacer'; | |
73 | else: | |
74 | return '~separator'; | |
75 | ||
76 | def _update_state(self, force_state=None): | |
77 | message = None | |
78 | if force_state is None: | |
79 | valid = False | |
80 | if self._check_callback is not None: | |
81 | check = self._check_callback(self._get_current_value()) | |
82 | if isinstance(check, str): | |
83 | message = check | |
84 | else: | |
85 | valid = bool(check) | |
86 | else: | |
87 | valid = True | |
88 | else: | |
89 | valid = force_state | |
90 | ||
91 | self._infobar.props.visible = message is not None | |
92 | if message is not None: | |
93 | self._message.props.label = message | |
94 | ||
95 | self._ok_button.props.sensitive = valid | |
96 | self._add_button.props.sensitive = valid | |
97 | ||
98 | def on_short_value_changed(self, widget): | |
99 | if not self._short_choice.props.active: | |
100 | self._short_choice.props.active = True | |
101 | else: | |
102 | self._update_state() | |
103 | ||
104 | def on_path_value_changed(self, widget): | |
105 | self._path_choice.props.active = True | |
106 | self._update_state() | |
107 | ||
108 | def on_short_choice_toggled(self, widget): | |
109 | self._update_state() | |
110 | ||
111 | def on_add_clicked(self, widget): | |
112 | value = self._get_current_value() | |
113 | if value: | |
114 | self._add_callback(value) | |
115 | self._update_state(False) | |
116 | ||
117 | def on_short_value_activate(self, entry): | |
118 | if self._short_choice.props.active and self._ok_button.props.sensitive: | |
119 | self._ok_button.clicked() | |
120 | ||
121 | def get_indicator(self): | |
122 | self._short_choice.props.active = True | |
123 | self._update_state() | |
124 | self._short_value.grab_focus() | |
125 | response = self.run() | |
126 | self.hide() | |
127 | if response == Gtk.ResponseType.OK: | |
128 | return self._get_current_value() | |
129 | return None |
0 | #!/usr/bin/env python3 | |
1 | # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- | |
2 | # LightDM GTK Greeter Settings | |
3 | # Copyright (C) 2014 Andrew P. <pan.pav.7c5@gmail.com> | |
4 | # | |
5 | # This program is free software: you can redistribute it and/or modify it | |
6 | # under the terms of the GNU General Public License version 3, as published | |
7 | # by the Free Software Foundation. | |
8 | # | |
9 | # This program is distributed in the hope that it will be useful, but | |
10 | # WITHOUT ANY WARRANTY; without even the implied warranties of | |
11 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | |
12 | # PURPOSE. See the GNU General Public License for more details. | |
13 | # | |
14 | # You should have received a copy of the GNU General Public License along | |
15 | # with this program. If not, see <http://www.gnu.org/licenses/>. | |
16 | ||
17 | ||
18 | import os | |
19 | import sys | |
20 | from copy import deepcopy | |
21 | from glob import iglob | |
22 | ||
23 | from gi.repository import Gtk | |
24 | ||
25 | from lightdm_gtk_greeter_settings import OptionEntry | |
26 | from lightdm_gtk_greeter_settings.helpers import ( | |
27 | C_, | |
28 | bool2string, | |
29 | string2bool, | |
30 | get_data_path, | |
31 | SimpleEnum, | |
32 | WidgetsEnum, | |
33 | WidgetsWrapper) | |
34 | from lightdm_gtk_greeter_settings.IndicatorsEntry import ( | |
35 | EmptyIndicators, | |
36 | Indicators, | |
37 | LayoutSet, | |
38 | Option) | |
39 | ||
40 | ||
41 | __all__ = ['IndicatorPropertiesDialog'] | |
42 | ||
43 | ||
44 | class IndicatorPath(OptionEntry.StringPathEntry): | |
45 | ||
46 | class Row(SimpleEnum): | |
47 | Title = () | |
48 | Type = () | |
49 | Icon = () | |
50 | ||
51 | ||
52 | class IndicatorIconEntry(OptionEntry.IconEntry): | |
53 | ||
54 | def __init__(self, widgets): | |
55 | super().__init__(widgets) | |
56 | self._label = widgets['label'] | |
57 | self._default_item = widgets['default_item'] | |
58 | self._default_item.connect('activate', self._on_select_default) | |
59 | ||
60 | def _set_value(self, value): | |
61 | if value is None: | |
62 | self._on_select_default() | |
63 | else: | |
64 | super()._set_value(value) | |
65 | ||
66 | def _update(self, icon=None, path=None, failed=False, default=None): | |
67 | super()._update(icon, path, failed) | |
68 | if default: | |
69 | markup = C_('option-entry|indicators', '<b>Using default value</b>') | |
70 | self._default_item.get_child().set_markup(markup) | |
71 | self._button.set_tooltip_markup(markup) | |
72 | self._image.props.visible = False | |
73 | else: | |
74 | self._default_item.get_child().set_markup( | |
75 | C_('option-entry|indicators', 'Use default value...')) | |
76 | self._image.props.visible = True | |
77 | self._label.props.label = self._button.get_tooltip_text() | |
78 | ||
79 | def _on_select_default(self, item=None): | |
80 | self._value = None | |
81 | self._image.props.icon_name = '' | |
82 | self._update(default=True) | |
83 | self._emit_changed() | |
84 | ||
85 | ||
86 | class IndicatorTypeEntry(OptionEntry.BaseEntry): | |
87 | ||
88 | def __init__(self, widgets): | |
89 | super().__init__(widgets) | |
90 | ||
91 | self._types = widgets['types'] | |
92 | self._indicator_choice = widgets['indicator_choice'] | |
93 | self._spacer_choice = widgets['spacer_choice'] | |
94 | self._separator_choice = widgets['separator_choice'] | |
95 | ||
96 | self._types.connect('changed', self._emit_changed) | |
97 | self._indicator_choice.connect('toggled', self._on_choice_changed, None, | |
98 | (self._types, widgets['indicator_box'])) | |
99 | self._spacer_choice.connect('toggled', self._on_choice_changed, Indicators.Spacer) | |
100 | self._separator_choice.connect('toggled', self._on_choice_changed, Indicators.Separator) | |
101 | ||
102 | self._value = None | |
103 | ||
104 | def add_type(self, name, title): | |
105 | if name not in EmptyIndicators: | |
106 | self._types.append(name, title or name) | |
107 | ||
108 | def _get_value(self): | |
109 | if self._indicator_choice.props.active: | |
110 | return self._types.props.active_id | |
111 | else: | |
112 | return self._value | |
113 | ||
114 | def _set_value(self, value): | |
115 | if value == Indicators.Spacer: | |
116 | button = self._spacer_choice | |
117 | elif value == Indicators.Separator: | |
118 | button = self._separator_choice | |
119 | else: | |
120 | button = self._indicator_choice | |
121 | ||
122 | self._value = value | |
123 | self._types.set_active_id(value) | |
124 | ||
125 | if button.props.active: | |
126 | button.toggled() | |
127 | else: | |
128 | button.props.active = True | |
129 | ||
130 | def _on_choice_changed(self, button, value, widgets=[]): | |
131 | for w in widgets: | |
132 | w.props.sensitive = button.props.active | |
133 | ||
134 | if button.props.active: | |
135 | self._value = value if value else self._types.props.active_id | |
136 | self._emit_changed() | |
137 | ||
138 | ||
139 | class IndicatorPropertiesDialog(Gtk.Dialog): | |
140 | __gtype_name__ = 'IndicatorPropertiesDialog' | |
141 | ||
142 | class Widgets(WidgetsEnum): | |
143 | add = 'add_button' | |
144 | ok = 'ok_button' | |
145 | infobar = 'infobar' | |
146 | message = 'message' | |
147 | custom_options = 'custom_options_box' | |
148 | path = 'option_path_combo' | |
149 | path_model = 'option_path_model' | |
150 | hide_disabled = 'option_power_hide_disabled' | |
151 | ||
152 | def __new__(cls, *args, **kwargs): | |
153 | builder = Gtk.Builder() | |
154 | builder.add_from_file(get_data_path('%s.ui' % cls.__name__)) | |
155 | window = builder.get_object('indicator_properties_dialog') | |
156 | window.builder = builder | |
157 | builder.connect_signals(window) | |
158 | window.init_window(*args, **kwargs) | |
159 | return window | |
160 | ||
161 | def init_window(self, is_duplicate=None, get_defaults=None, get_name=str): | |
162 | self._widgets = self.Widgets(builder=self.builder) | |
163 | self._get_defaults = get_defaults | |
164 | self._add_indicator = None | |
165 | self._is_duplicate = is_duplicate | |
166 | self._get_name = get_name | |
167 | self._indicator_loaded = False | |
168 | self._name = None | |
169 | self._reversed = False | |
170 | ||
171 | self._name2page = {} | |
172 | for i in range(0, self._widgets.custom_options.get_n_pages()): | |
173 | page = self._widgets.custom_options.get_nth_page(i) | |
174 | name = Gtk.Buildable.get_name(page) | |
175 | self._name2page['~' + name.rsplit('_')[-1]] = i | |
176 | ||
177 | self._option_type = IndicatorTypeEntry(WidgetsWrapper(self.builder, 'option_type')) | |
178 | self._option_text = OptionEntry.StringEntry(WidgetsWrapper(self.builder, 'option_text')) | |
179 | self._option_image = IndicatorIconEntry(WidgetsWrapper(self.builder, 'option_image')) | |
180 | self._option_path = IndicatorPath(WidgetsWrapper(self.builder, 'option_path')) | |
181 | self._option_hide_disabled = \ | |
182 | OptionEntry.BooleanEntry(WidgetsWrapper(self.builder, 'option_hide_disabled')) | |
183 | ||
184 | for entry in (self._option_type, self._option_path): | |
185 | entry.changed.connect(self._on_option_changed) | |
186 | ||
187 | for name in Indicators: | |
188 | self._option_type.add_type(name, self._get_name(name)) | |
189 | ||
190 | # Hiding first column created by Gtk.ComboBoxText | |
191 | self._widgets.path.get_cells()[0].props.visible = False | |
192 | ||
193 | for path in sorted(iglob(os.path.join(sys.prefix, 'share', 'unity', 'indicators', '*'))): | |
194 | name = os.path.basename(path) | |
195 | parts = name.rsplit('.', maxsplit=1) | |
196 | if len(parts) == 2 and parts[0] == 'com.canonical.indicator': | |
197 | name = parts[1] | |
198 | row = IndicatorPath.Row._make(Type=IndicatorPath.ItemType.Value, | |
199 | Title=name, | |
200 | Icon='application-x-executable') | |
201 | self._widgets.path_model.append(row) | |
202 | ||
203 | for path in sorted(iglob(os.path.join(sys.prefix, 'lib', 'indicators3', '7', '*.so'))): | |
204 | row = IndicatorPath.Row._make(Type=IndicatorPath.ItemType.Value, | |
205 | Title=os.path.basename(path), | |
206 | Icon='application-x-executable') | |
207 | self._widgets.path_model.append(row) | |
208 | ||
209 | def _on_option_changed(self, entry=None): | |
210 | if not self._indicator_loaded: | |
211 | return | |
212 | ||
213 | name = self._option_type.value | |
214 | error = None | |
215 | warning = None | |
216 | ||
217 | if name == Indicators.External: | |
218 | if not str(self._option_path.value).strip(): | |
219 | error = C_('option-entry|indicators', 'Path/Service field is not filled') | |
220 | elif name != self._name: | |
221 | if self._is_duplicate and self._is_duplicate(name): | |
222 | warning = C_('option-entry|indicators', | |
223 | 'Indicator "{name}" is already in the list.\n' | |
224 | 'It will be overwritten.').format(name=self._get_name(name, name)) | |
225 | ||
226 | self._widgets.ok.props.sensitive = error is None | |
227 | self._widgets.add.props.sensitive = error is None | |
228 | self._widgets.infobar.props.visible = error or warning | |
229 | self._widgets.message.props.label = error or warning | |
230 | ||
231 | if error: | |
232 | self._widgets.infobar.props.message_type = Gtk.MessageType.WARNING | |
233 | elif warning: | |
234 | self._widgets.infobar.props.message_type = Gtk.MessageType.INFO | |
235 | else: | |
236 | self._widgets.infobar.props.message_type = Gtk.MessageType.OTHER | |
237 | ||
238 | def on_option_type_types_changed(self, combo): | |
239 | current = self._widgets.custom_options.props.page | |
240 | if current != -1: | |
241 | self._widgets.custom_options.get_nth_page(current).props.visible = False | |
242 | current = self._name2page.get(combo.props.active_id, -1) | |
243 | if current != -1: | |
244 | self._widgets.custom_options.get_nth_page(current).props.visible = True | |
245 | self._widgets.custom_options.props.page = current | |
246 | if self._indicator_loaded: | |
247 | defaults = self._get_defaults(combo.props.active_id) | |
248 | self._option_text.enabled = Option.Text in defaults | |
249 | self._option_image.enabled = Option.Image in defaults | |
250 | ||
251 | def on_add_clicked(self, widget): | |
252 | self._add_callback(self.get_indicator()) | |
253 | self._options = deepcopy(self._options) | |
254 | self._on_option_changed() | |
255 | ||
256 | @property | |
257 | def add_callback(self): | |
258 | return self._add_callback | |
259 | ||
260 | @add_callback.setter | |
261 | def add_callback(self, value): | |
262 | self._add_callback = value | |
263 | self._widgets.add.props.visible = value is not None | |
264 | ||
265 | def set_indicator(self, options): | |
266 | self._indicator_loaded = False | |
267 | self._options = deepcopy(options) | |
268 | self._name = options[Option.Name] | |
269 | ||
270 | self._option_type.value = options[Option.Name] | |
271 | self._option_path.value = options.get(Option.Path) | |
272 | ||
273 | self._option_text.value = options.get(Option.Text, '') | |
274 | self._option_text.enabled = Option.Text in options | |
275 | ||
276 | self._option_image.value = options.get(Option.Image) | |
277 | self._option_image.enabled = Option.Image in options | |
278 | ||
279 | self._reversed = Option.Layout in options and LayoutSet.Reversed in options[Option.Layout] | |
280 | ||
281 | hide_disabled = options.get(Option.HideDisabled, bool2string(False)) | |
282 | self._option_hide_disabled.value = hide_disabled or bool2string(True) | |
283 | ||
284 | self._indicator_loaded = True | |
285 | self._on_option_changed() | |
286 | ||
287 | def get_indicator(self): | |
288 | options = self._options | |
289 | ||
290 | name = self._option_type.value | |
291 | options[Option.Name] = name | |
292 | ||
293 | options[Option.Layout] = set() | |
294 | if name not in EmptyIndicators: | |
295 | if self._option_text.enabled: | |
296 | options[Option.Text] = self._option_text.value or None | |
297 | options[Option.Layout].add(LayoutSet.Text) | |
298 | if self._option_image.enabled: | |
299 | options[Option.Image] = self._option_image.value or None | |
300 | options[Option.Layout].add(LayoutSet.Image) | |
301 | if self._option_text.enabled and self._option_image.enabled and self._reversed: | |
302 | options[Option.Layout].add(LayoutSet.Reversed) | |
303 | ||
304 | if LayoutSet.Text not in options[Option.Layout] and Option.Text in options: | |
305 | del options[Option.Text] | |
306 | if LayoutSet.Image not in options[Option.Layout] and Option.Image in options: | |
307 | del options[Option.Image] | |
308 | ||
309 | if name == Indicators.External: | |
310 | options[Option.Path] = self._option_path.value | |
311 | else: | |
312 | options.pop(Option.Path, None) | |
313 | ||
314 | if name == Indicators.Power and string2bool(self._option_hide_disabled.value): | |
315 | options[Option.HideDisabled] = None | |
316 | elif Option.HideDisabled in options: | |
317 | options.pop(Option.HideDisabled, None) | |
318 | ||
319 | return options |
0 | # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- | |
1 | # LightDM GTK Greeter Settings | |
2 | # Copyright (C) 2014 Andrew P. <pan.pav.7c5@gmail.com> | |
3 | # | |
4 | # This program is free software: you can redistribute it and/or modify it | |
5 | # under the terms of the GNU General Public License version 3, as published | |
6 | # by the Free Software Foundation. | |
7 | # | |
8 | # This program is distributed in the hope that it will be useful, but | |
9 | # WITHOUT ANY WARRANTY; without even the implied warranties of | |
10 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | |
11 | # PURPOSE. See the GNU General Public License for more details. | |
12 | # | |
13 | # You should have received a copy of the GNU General Public License along | |
14 | # with this program. If not, see <http://www.gnu.org/licenses/>. | |
15 | ||
16 | ||
17 | import operator | |
18 | import platform | |
19 | import string | |
20 | from copy import deepcopy | |
21 | ||
22 | from gi.repository import ( | |
23 | Gtk, | |
24 | Gdk) | |
25 | from gi.repository.GObject import markup_escape_text as escape_markup | |
26 | ||
27 | from lightdm_gtk_greeter_settings.helpers import ( | |
28 | C_, | |
29 | get_markup_error, | |
30 | SimpleEnum, | |
31 | TreeStoreDataWrapper) | |
32 | from lightdm_gtk_greeter_settings.OptionEntry import BaseEntry | |
33 | ||
34 | ||
35 | __all__ = ['BuiltInIndicators', | |
36 | 'EmptyIndicators', | |
37 | 'Indicators', | |
38 | 'IndicatorsEntry', | |
39 | 'Option', | |
40 | 'SingleIndicators'] | |
41 | ||
42 | ||
43 | class Indicators(SimpleEnum): | |
44 | External = '~external' | |
45 | Spacer = '~spacer' | |
46 | Separator = '~separator' | |
47 | Text = '~text' | |
48 | Host = '~host' | |
49 | Clock = '~clock' | |
50 | Layout = '~layout' | |
51 | Session = '~session' | |
52 | Language = '~language' | |
53 | A11y = '~a11y' | |
54 | Power = '~power' | |
55 | ||
56 | ||
57 | # Valid builtin indicators | |
58 | BuiltInIndicators = set(Indicators) - {Indicators.External} | |
59 | ||
60 | # Special indicators | |
61 | EmptyIndicators = {Indicators.Spacer, Indicators.Separator} | |
62 | ||
63 | # These indicators can have only one instance | |
64 | SingleIndicators = set(Indicators) - {Indicators.External, Indicators.Text, | |
65 | Indicators.Spacer, Indicators.Separator} | |
66 | ||
67 | # Valid options names | |
68 | ||
69 | ||
70 | class Option(SimpleEnum): | |
71 | # Common | |
72 | Name = 'name' | |
73 | Layout = 'layout' | |
74 | Text = 'text' | |
75 | Image = 'image' | |
76 | FallbackImage = 'fallback-image' | |
77 | Tooltip = 'tooltip' | |
78 | Markup = 'markup' | |
79 | Expand = 'expand' | |
80 | Align = 'align' | |
81 | # External | |
82 | Path = 'path' | |
83 | # Power | |
84 | HideDisabled = 'hide-disabled' | |
85 | ||
86 | ||
87 | class Layout(SimpleEnum): | |
88 | Empty = '' | |
89 | Text = 'text' | |
90 | Image = 'image' | |
91 | ImageText = 'image-text' | |
92 | TextImage = 'text-image' | |
93 | ||
94 | @classmethod | |
95 | def _to_set(cls, value): | |
96 | return LayoutSet._str2set.get(value, set()) | |
97 | ||
98 | ||
99 | class LayoutSet(SimpleEnum): | |
100 | Text = 'text' | |
101 | Image = 'image' | |
102 | Reversed = 'reversed' | |
103 | ||
104 | _str2set = { | |
105 | Layout.Empty: set(), | |
106 | Layout.Text: {Text}, | |
107 | Layout.Image: {Image}, | |
108 | Layout.ImageText: {Text, Image}, | |
109 | Layout.TextImage: {Text, Image, Reversed}} | |
110 | ||
111 | @classmethod | |
112 | def _to_string(cls, value): | |
113 | return next((k for k, v in cls._str2set.items() if v == value), '') | |
114 | ||
115 | ||
116 | class Row(SimpleEnum): | |
117 | Name = () | |
118 | Tooltip = () | |
119 | HasState = () | |
120 | State = () | |
121 | Options = () | |
122 | Icon = () | |
123 | Markup = () | |
124 | ||
125 | ||
126 | class IndicatorsEntry(BaseEntry): | |
127 | # Readable names for indicators | |
128 | Names = { | |
129 | Indicators.External: C_('option-entry|indicators|name', 'External library/service'), | |
130 | Indicators.Spacer: C_('option-entry|indicators|name', 'Spacer'), | |
131 | Indicators.Separator: C_('option-entry|indicators|name', 'Separator'), | |
132 | Indicators.Text: C_('option-entry|indicators|name', 'Text'), | |
133 | Indicators.Clock: C_('option-entry|indicators|name', 'Clock'), | |
134 | Indicators.Host: C_('option-entry|indicators|name', 'Host name'), | |
135 | Indicators.Layout: C_('option-entry|indicators|name', 'Keyboard layout'), | |
136 | Indicators.Session: C_('option-entry|indicators|name', 'Sessions menu'), | |
137 | Indicators.Language: C_('option-entry|indicators|name', 'Languages menu'), | |
138 | Indicators.A11y: C_('option-entry|indicators|name', 'Accessibility menu'), | |
139 | Indicators.Power: C_('option-entry|indicators|name', 'Power menu')} | |
140 | # Default icons for indicators to display in treeview | |
141 | Icons = { | |
142 | Indicators.A11y: 'preferences-desktop-accessibility', | |
143 | Indicators.Session: 'document-properties', | |
144 | Indicators.Power: 'system-shutdown'} | |
145 | Tooltips = { | |
146 | Indicators.Spacer: C_('option-entry|indicators|tooltip', 'Spacer'), | |
147 | Indicators.Separator: C_('option-entry|indicators|tooltip', 'Separator'), | |
148 | Indicators.Text: C_('option-entry|indicators|tooltip', 'Custom text or/and image'), | |
149 | Indicators.Host: C_('option-entry|indicators|tooltip', 'Host name'), | |
150 | Indicators.Clock: C_('option-entry|indicators|tooltip', 'Clock'), | |
151 | Indicators.Layout: C_('option-entry|indicators|tooltip', 'Layout indicator'), | |
152 | Indicators.Session: C_('option-entry|indicators|tooltip', | |
153 | 'Sessions menu (xfce, unity, gnome etc.)'), | |
154 | Indicators.Language: C_('option-entry|indicators|tooltip', 'Languages menu'), | |
155 | Indicators.A11y: C_('option-entry|indicators|tooltip', 'Accessibility menu'), | |
156 | Indicators.Power: C_('option-entry|indicators|tooltip', 'Power menu')} | |
157 | # Default options for indicators | |
158 | DefaultOptions = { | |
159 | Indicators.External: {Option.Text: None, Option.Image: None}, | |
160 | Indicators.Spacer: {Option.Layout: set()}, | |
161 | Indicators.Separator: {Option.Layout: set()}, | |
162 | Indicators.Text: {Option.Layout: {LayoutSet.Text}, Option.Text: None}, | |
163 | Indicators.Host: {Option.Layout: {LayoutSet.Text}, Option.Text: None}, | |
164 | Indicators.Clock: {Option.Layout: {LayoutSet.Text}, Option.Text: None}, | |
165 | Indicators.Layout: {Option.Layout: {LayoutSet.Text}, Option.Text: None}, | |
166 | Indicators.Session: {Option.Layout: {LayoutSet.Text, LayoutSet.Image}, | |
167 | Option.Text: None, Option.Image: None}, | |
168 | Indicators.Language: {Option.Layout: {LayoutSet.Text}, Option.Text: None}, | |
169 | Indicators.A11y: {Option.Layout: {LayoutSet.Image}, Option.Image: None}, | |
170 | Indicators.Power: {Option.Layout: {LayoutSet.Image}, Option.Image: None}} | |
171 | ||
172 | def __init__(self, widgets): | |
173 | super().__init__(widgets) | |
174 | ||
175 | for k, v in self.DefaultOptions.items(): | |
176 | v[Option.Name] = k | |
177 | ||
178 | self._treeview = widgets['treeview'] | |
179 | self._selection = widgets['selection'] | |
180 | self._state_renderer = widgets['state_renderer'] | |
181 | self._state_column = widgets['state_column'] | |
182 | self._add = widgets['add'] | |
183 | self._remove = widgets['remove'] | |
184 | self._up = widgets['up'] | |
185 | self._down = widgets['down'] | |
186 | self._tools = widgets['tools'] | |
187 | self._model = widgets['model'] | |
188 | self._widgets_to_disable = [self._treeview, widgets['toolbar']] | |
189 | self._properties_dialog = None | |
190 | self._row_menu = None | |
191 | self._tools_menu = None | |
192 | self._show_unused = False | |
193 | ||
194 | self._treeview.connect('key-press-event', self._on_key_press) | |
195 | self._treeview.connect('row-activated', self._on_row_activated) | |
196 | self._treeview.connect('button-release-event', self._on_button_release) | |
197 | self._selection.connect('changed', self._on_selection_changed) | |
198 | self._state_renderer.connect('toggled', self._on_state_toggled) | |
199 | ||
200 | self._add.connect('clicked', self._on_add_clicked) | |
201 | self._remove.connect('clicked', self._on_remove_clicked) | |
202 | self._up.connect('clicked', self._on_up_clicked) | |
203 | self._down.connect('clicked', self._on_down_clicked) | |
204 | self._tools.connect('clicked', self._on_tools_clicked) | |
205 | ||
206 | self._on_row_changed_id = self._model.connect('row-changed', self._on_model_changed) | |
207 | self._on_row_deleted_id = self._model.connect('row-deleted', self._on_model_changed) | |
208 | self._on_row_inserted_id = self._model.connect('row-inserted', self._on_model_changed) | |
209 | self._on_rows_reordered_id = self._model.connect('rows-reordered', self._on_model_changed) | |
210 | ||
211 | def _on_model_changed(self, *unused): | |
212 | self._emit_changed() | |
213 | ||
214 | def _get_value(self): | |
215 | def fix_token(s): | |
216 | s = s.replace('"', r'\"') | |
217 | if any(c in s for c in string.whitespace): | |
218 | s = '"' + s + '"' | |
219 | return s | |
220 | ||
221 | items = [] | |
222 | for row in self._model: | |
223 | if row[Row.HasState] and not row[Row.State]: | |
224 | continue | |
225 | ||
226 | options = deepcopy(row[Row.Options].data) | |
227 | name = options.pop(Option.Name) | |
228 | defaults = deepcopy(self.DefaultOptions[name]) | |
229 | ||
230 | # text, image, layout=image-text -> text, image | |
231 | if options.get(Option.Layout) == {LayoutSet.Text, LayoutSet.Image}: | |
232 | del options[Option.Layout] | |
233 | # defaults.pop(Option.Image, None) | |
234 | # defaults.pop(Option.Text, None) | |
235 | ||
236 | for k in defaults.keys() & options.keys(): | |
237 | if defaults[k] == options[k]: | |
238 | del options[k] | |
239 | ||
240 | if Option.Layout in options: | |
241 | layout = options[Option.Layout] | |
242 | options[Option.Layout] = LayoutSet._to_string(layout) | |
243 | # text, layout=text -> layout=text | |
244 | if LayoutSet.Text in layout and options.get(Option.Text, self) is None: | |
245 | del options[Option.Text] | |
246 | if LayoutSet.Image in layout and options.get(Option.Image, self) is None: | |
247 | del options[Option.Image] | |
248 | ||
249 | # name=~text, text=value -> ~~value | |
250 | if name == Indicators.Text: | |
251 | name = '~~' + (options.pop(Option.Text, None) or '') | |
252 | elif name == Indicators.External: | |
253 | name = options.pop(Option.Path, None) or '' | |
254 | ||
255 | if not options: | |
256 | items.append(fix_token(name)) | |
257 | else: | |
258 | values = (fix_token(k) + '=' + fix_token(v) if v else fix_token(k) | |
259 | for k, v in sorted(options.items(), key=operator.itemgetter(0))) | |
260 | items.append(fix_token(name) + ': ' + ', '.join(values)) | |
261 | return '; '.join(items) | |
262 | ||
263 | def _set_value(self, value): | |
264 | with self._model.handler_block(self._on_row_deleted_id): | |
265 | self._model.clear() | |
266 | ||
267 | for options in self._read_options_string(value): | |
268 | name = options[Option.Name] | |
269 | ||
270 | if name.startswith('~~'): | |
271 | options.setdefault(Option.Text, name[2:]) | |
272 | options[Option.Name] = Indicators.Text | |
273 | name = Indicators.Text | |
274 | elif name not in BuiltInIndicators: | |
275 | options.setdefault(Option.Path, name) | |
276 | options[Option.Name] = Indicators.External | |
277 | name = Indicators.External | |
278 | ||
279 | defaults = deepcopy(self.DefaultOptions[name]) | |
280 | ||
281 | if Option.Markup in options: | |
282 | markup = options[Option.Markup] | |
283 | if markup is not None: | |
284 | options[Option.Text] = markup | |
285 | options[Option.Markup] = None | |
286 | ||
287 | if Option.Layout in options: | |
288 | options[Option.Layout] = Layout._to_set(options[Option.Layout]) | |
289 | else: | |
290 | options[Option.Layout] = defaults.get(Option.Layout) or set() | |
291 | ||
292 | if Option.Text in options: | |
293 | options[Option.Layout].add(LayoutSet.Text) | |
294 | elif LayoutSet.Text in options[Option.Layout]: | |
295 | options.setdefault(Option.Text, None) | |
296 | else: | |
297 | defaults.pop(Option.Text, None) | |
298 | ||
299 | if Option.Image in options: | |
300 | options[Option.Layout].add(LayoutSet.Image) | |
301 | elif LayoutSet.Image in options[Option.Layout]: | |
302 | options.setdefault(Option.Image, None) | |
303 | else: | |
304 | defaults.pop(Option.Image, None) | |
305 | ||
306 | options.update((k, defaults[k]) | |
307 | for k in defaults.keys() - options.keys()) | |
308 | ||
309 | with self._model.handler_block(self._on_row_changed_id), \ | |
310 | self._model.handler_block(self._on_row_inserted_id): | |
311 | self._set_row(None, options, select=False) | |
312 | ||
313 | if self._show_unused: | |
314 | self._tools_show_unused_toggled() | |
315 | ||
316 | self._selection.select_path(0) | |
317 | self._on_model_changed() | |
318 | ||
319 | def _read_options_string(self, s): | |
320 | ||
321 | while s: | |
322 | name, s = self._next_string_token(s, ':;') | |
323 | if not name: | |
324 | continue | |
325 | options = {Option.Name: name} | |
326 | ||
327 | if s.startswith(':'): | |
328 | while s: | |
329 | option, s = self._next_string_token(s[1:], '=,;') | |
330 | if s.startswith('='): | |
331 | value, s = self._next_string_token(s[1:], ',;') | |
332 | else: | |
333 | value = None | |
334 | options[option] = value | |
335 | if not s.startswith(','): | |
336 | break | |
337 | ||
338 | yield options | |
339 | s = s[1:] | |
340 | ||
341 | def _next_string_token(self, s, delimiters): | |
342 | token = [] | |
343 | quoted = False | |
344 | ||
345 | for last, c in enumerate(s): | |
346 | if not c.isspace(): | |
347 | break | |
348 | ||
349 | # Parsing quotes | |
350 | for i, c in enumerate(s[last:], last): | |
351 | if c == '"': | |
352 | if i > last and s[i - 1] == '\\': | |
353 | token.append(s[last:i - 1]) | |
354 | token.append('"') | |
355 | else: | |
356 | token.append(s[last:i]) | |
357 | quoted = not quoted | |
358 | last = i + 1 | |
359 | elif not quoted and c in delimiters: | |
360 | break | |
361 | ||
362 | if quoted: | |
363 | return '', '' | |
364 | ||
365 | if last != i or last == 0: | |
366 | token.append(s[last: i if c in delimiters else i + 1].rstrip()) | |
367 | ||
368 | return ''.join(token) if token else None, s[i:] | |
369 | ||
370 | def _remove_selection(self): | |
371 | model, rowiter = self._selection.get_selected() | |
372 | if rowiter: | |
373 | if self._show_unused and model[rowiter][Row.HasState]: | |
374 | model[rowiter][Row.State] = False | |
375 | else: | |
376 | model.remove(rowiter) | |
377 | self._on_selection_changed() | |
378 | ||
379 | def _move_selection(self, move_up): | |
380 | model, rowiter = self._selection.get_selected() | |
381 | if rowiter: | |
382 | next_iter = model.iter_previous( | |
383 | rowiter) if move_up else model.iter_next(rowiter) | |
384 | if self._show_unused and \ | |
385 | (model[rowiter][Row.HasState] and not model[rowiter][Row.State] or | |
386 | model[next_iter][Row.HasState] and not model[next_iter][Row.State]): | |
387 | with self._model.handler_block(self._on_rows_reordered_id): | |
388 | model.swap(rowiter, next_iter) | |
389 | else: | |
390 | model.swap(rowiter, next_iter) | |
391 | self._on_selection_changed() | |
392 | ||
393 | def _create_row_tuple(self, options): | |
394 | name = options[Option.Name] | |
395 | error = None | |
396 | ||
397 | text = options.get(Option.Text) | |
398 | if Option.Text in options: | |
399 | if text is not None: | |
400 | if Option.Markup in options: | |
401 | error = get_markup_error(text) | |
402 | if error: | |
403 | text = '<i>{text}</i>'.format(text=escape_markup(text)) | |
404 | else: | |
405 | text = escape_markup(text) | |
406 | text = '"' + text + '"' | |
407 | elif name == Indicators.Host: | |
408 | text = escape_markup(platform.node()) | |
409 | ||
410 | display_name = self.Names.get(name, name) | |
411 | if name == Indicators.External: | |
412 | if options.get(Option.Path): | |
413 | title = '{name} ({value})'.format(name=escape_markup(display_name), | |
414 | value=escape_markup(options[Option.Path])) | |
415 | else: | |
416 | title = escape_markup(display_name) | |
417 | else: | |
418 | title = escape_markup(display_name) | |
419 | ||
420 | if text: | |
421 | markup = '{name}: {text}'.format(name=title, text=text) | |
422 | else: | |
423 | markup = title | |
424 | ||
425 | if Option.Image in options: | |
426 | icon = options[Option.Image] | |
427 | if icon and icon.startswith('#'): | |
428 | icon = icon[1:] | |
429 | elif icon: | |
430 | icon = 'image-x-generic' | |
431 | else: | |
432 | if name in self.Icons: | |
433 | icon = self.Icons[name] | |
434 | elif name in BuiltInIndicators: | |
435 | icon = 'applications-system' | |
436 | else: | |
437 | icon = 'application-x-executable' | |
438 | else: | |
439 | icon = '' | |
440 | ||
441 | has_state = name in SingleIndicators | |
442 | ||
443 | return Row._make(Name=name, | |
444 | Tooltip=self.Tooltips.get(name), | |
445 | Icon=icon, | |
446 | Markup=markup, | |
447 | HasState=has_state, State=has_state, | |
448 | Options=TreeStoreDataWrapper(options)) | |
449 | ||
450 | def _set_row(self, rowiter, options, select=True): | |
451 | old_name = self._model[rowiter][Row.Name] if rowiter else None | |
452 | new_name = options.get( | |
453 | Option.Name, '') if options is not None else None | |
454 | old_is_single = old_name in SingleIndicators | |
455 | new_is_single = new_name in SingleIndicators | |
456 | ||
457 | if new_name == old_name: | |
458 | # The same row - just update | |
459 | pass | |
460 | elif old_is_single and new_is_single: | |
461 | old_row = next( | |
462 | (row for row in self._model if row[Row.Name] == new_name), None) | |
463 | if old_row: | |
464 | if self._show_unused: | |
465 | # Swap current row with new_row | |
466 | with self._model.handler_block(self._on_rows_reordered_id): | |
467 | self._model.move_before(old_row.iter, rowiter) | |
468 | with self._model.handler_block(self._on_row_changed_id): | |
469 | self._model[rowiter][Row.State] = False | |
470 | rowiter = old_row.iter | |
471 | else: | |
472 | # Replace current row with replace_row | |
473 | with self._model.handler_block(self._on_row_deleted_id): | |
474 | self._model.remove(old_row.iter) | |
475 | elif old_is_single: | |
476 | if self._show_unused: | |
477 | # Uncheck old row and use new instead of it | |
478 | with self._model.handler_block(self._on_row_changed_id): | |
479 | self._model[rowiter][Row.State] = False | |
480 | with self._model.handler_block(self._on_row_inserted_id): | |
481 | new_iter = self._model.insert_after(rowiter) | |
482 | rowiter = new_iter | |
483 | elif new_is_single: | |
484 | old_row = next( | |
485 | (row for row in self._model if row[Row.Name] == new_name), None) | |
486 | if old_row: | |
487 | with self._model.handler_block(self._on_row_deleted_id): | |
488 | self._model.remove(old_row.iter) | |
489 | ||
490 | if rowiter and options: | |
491 | with self._model.handler_block(self._on_row_changed_id): | |
492 | self._model[rowiter] = self._create_row_tuple(options) | |
493 | self._model.row_changed(self._model.get_path(rowiter), rowiter) | |
494 | elif options: | |
495 | rowiter = self._model.append(self._create_row_tuple(options)) | |
496 | ||
497 | if select and rowiter: | |
498 | self._selection.select_iter(rowiter) | |
499 | ||
500 | return rowiter | |
501 | ||
502 | def _edit_indicator(self, options, add_callback=None): | |
503 | if not self._properties_dialog: | |
504 | from lightdm_gtk_greeter_settings.IndicatorPropertiesDialog \ | |
505 | import IndicatorPropertiesDialog as Dialog | |
506 | self._properties_dialog = Dialog(is_duplicate=self._is_duplicate, | |
507 | get_defaults=self.DefaultOptions.get, | |
508 | get_name=self.Names.get) | |
509 | self._properties_dialog.props.transient_for = self._treeview.get_toplevel() | |
510 | ||
511 | self._properties_dialog.add_callback = add_callback | |
512 | self._properties_dialog.set_indicator(options) | |
513 | if self._properties_dialog.run() == Gtk.ResponseType.OK: | |
514 | options = self._properties_dialog.get_indicator() | |
515 | else: | |
516 | options = None | |
517 | self._properties_dialog.hide() | |
518 | return options | |
519 | ||
520 | def _is_duplicate(self, name): | |
521 | return name in SingleIndicators and any(row[Row.Name] == name | |
522 | for row in self._model if row[Row.State]) | |
523 | ||
524 | def _add_indicator(self, options): | |
525 | self._set_row(None, options) | |
526 | ||
527 | def _on_key_press(self, treeview, event): | |
528 | if Gdk.keyval_name(event.keyval) == 'Delete': | |
529 | self._remove_selection() | |
530 | elif Gdk.keyval_name(event.keyval) == 'F2': | |
531 | model, rowiter = self._selection.get_selected() | |
532 | treeview.row_activated(model.get_path(rowiter), None) | |
533 | else: | |
534 | return False | |
535 | return True | |
536 | ||
537 | def _on_row_activated(self, treeview, path, column): | |
538 | if column != self._state_column: | |
539 | options = self._edit_indicator(self._model[path][Row.Options].data) | |
540 | if options: | |
541 | self._set_row(self._model.get_iter(path), options) | |
542 | ||
543 | def _on_button_release(self, treeview, event): | |
544 | if event.button != 3: | |
545 | return False | |
546 | ||
547 | pos = treeview.get_path_at_pos(int(event.x), int(event.y)) | |
548 | if not pos: | |
549 | return False | |
550 | ||
551 | row = self._model[pos[0]] | |
552 | if row[Row.HasState] and not row[Row.State]: | |
553 | return False | |
554 | ||
555 | if not self._row_menu: | |
556 | self._row_menu = Gtk.Menu() | |
557 | self._row_menu_reset = Gtk.MenuItem(C_('option-entry|indicators', | |
558 | 'Reset to _defaults')) | |
559 | self._row_menu_text = Gtk.CheckMenuItem(C_('option-entry|indicators', | |
560 | 'Display _label')) | |
561 | self._row_menu_image = Gtk.CheckMenuItem(C_('option-entry|indicators', | |
562 | 'Display _image')) | |
563 | self._row_menu_remove = Gtk.MenuItem( | |
564 | C_('option-entry|indicators', '_Remove')) | |
565 | ||
566 | self._row_menu_text_id = self._row_menu_text.connect('toggled', | |
567 | self._on_row_menu_toggled, | |
568 | Option.Text) | |
569 | self._row_menu_image_id = self._row_menu_image.connect('toggled', | |
570 | self._on_row_menu_toggled, | |
571 | Option.Image) | |
572 | self._row_menu_reset.connect( | |
573 | 'activate', self._on_row_menu_reset_clicked) | |
574 | self._row_menu_remove.connect('activate', self._on_remove_clicked) | |
575 | ||
576 | self._row_menu.append(self._row_menu_reset) | |
577 | self._row_menu.append(self._row_menu_text) | |
578 | self._row_menu.append(self._row_menu_image) | |
579 | self._row_menu.append(Gtk.SeparatorMenuItem()) | |
580 | self._row_menu.append(self._row_menu_remove) | |
581 | ||
582 | for item in self._row_menu: | |
583 | if type(item) is not Gtk.SeparatorMenuItem: | |
584 | item.props.use_underline = True | |
585 | item.props.visible = True | |
586 | ||
587 | options = row[Row.Options].data | |
588 | ||
589 | with self._row_menu_text.handler_block(self._row_menu_text_id): | |
590 | self._row_menu_text.props.active = Option.Text in options | |
591 | with self._row_menu_image.handler_block(self._row_menu_image_id): | |
592 | self._row_menu_image.props.active = Option.Image in options | |
593 | ||
594 | editable = options[Option.Name] not in { | |
595 | Indicators.Spacer, Indicators.Separator} | |
596 | self._row_menu_reset.props.sensitive = editable | |
597 | self._row_menu_text.props.sensitive = editable | |
598 | self._row_menu_image.props.sensitive = editable | |
599 | ||
600 | self._row_menu.popup(None, None, None, None, 0, | |
601 | Gtk.get_current_event_time()) | |
602 | ||
603 | return True | |
604 | ||
605 | def _on_row_menu_reset_clicked(self, item): | |
606 | model, rowiter = self._selection.get_selected() | |
607 | if rowiter: | |
608 | name = model[rowiter][Row.Name] | |
609 | options = deepcopy(self.DefaultOptions[name]) | |
610 | options[Option.Name] = name | |
611 | with model.handler_block(self._on_row_changed_id): | |
612 | model[rowiter] = self._create_row_tuple(options) | |
613 | model.row_changed(model.get_path(rowiter), rowiter) | |
614 | ||
615 | def _on_row_menu_toggled(self, item, option): | |
616 | model, rowiter = self._selection.get_selected() | |
617 | options = model[rowiter][Row.Options].data | |
618 | if item.props.active: | |
619 | options.setdefault(option, None) | |
620 | else: | |
621 | options.pop(option, None) | |
622 | model[rowiter] = self._create_row_tuple(options) | |
623 | ||
624 | def _on_state_toggled(self, renderer, path): | |
625 | self._model[path][Row.State] = not self._model[path][Row.State] | |
626 | ||
627 | def _on_selection_changed(self, selection=None): | |
628 | model, rowiter = self._selection.get_selected() | |
629 | if rowiter: | |
630 | row = model[rowiter] | |
631 | self._remove.props.sensitive = not row[ | |
632 | Row.HasState] or row[Row.State] | |
633 | self._down.props.sensitive = model.iter_next(rowiter) | |
634 | self._up.props.sensitive = model.iter_previous(rowiter) | |
635 | self._treeview.scroll_to_cell(model.get_path(rowiter)) | |
636 | else: | |
637 | self._remove.props.sensitive = False | |
638 | self._down.props.sensitive = False | |
639 | self._up.props.sensitive = False | |
640 | ||
641 | def _on_add_clicked(self, button=None): | |
642 | options = self._edit_indicator(self.DefaultOptions[Indicators.External], | |
643 | add_callback=self._add_indicator) | |
644 | if options: | |
645 | self._set_row(None, options, select=True) | |
646 | ||
647 | def _on_remove_clicked(self, button=None): | |
648 | self._remove_selection() | |
649 | ||
650 | def _on_up_clicked(self, button=None): | |
651 | self._move_selection(move_up=True) | |
652 | ||
653 | def _on_down_clicked(self, button=None): | |
654 | self._move_selection(move_up=False) | |
655 | ||
656 | def _on_tools_clicked(self, button=None): | |
657 | if not self._tools_menu: | |
658 | self._tools_menu = Gtk.Menu() | |
659 | self._tools_menu.attach_to_widget(self._tools) | |
660 | ||
661 | unused_item = Gtk.CheckMenuItem( | |
662 | C_('option-entry|indicators', 'Show unused items')) | |
663 | unused_item.connect('toggled', self._tools_show_unused_toggled) | |
664 | self._tools_menu.append(unused_item) | |
665 | ||
666 | header_item = Gtk.MenuItem( | |
667 | C_('option-entry|indicators', 'Predefined templates:')) | |
668 | header_item.props.sensitive = False | |
669 | self._tools_menu.append(Gtk.SeparatorMenuItem()) | |
670 | self._tools_menu.append(header_item) | |
671 | ||
672 | templates = ( | |
673 | ('host ~ clock, language, session, power', | |
674 | '~host;~spacer;~language;~session;~power'), | |
675 | ('host ~ clock ~ language, session, a11y, power', | |
676 | '~host;~spacer;~clock;~spacer;~language;~session;~a11y;~power'), | |
677 | ('host, layout, clock ~ language, session, power', | |
678 | '~host;~layout;~clock;~spacer;~language;~session;~power')) | |
679 | ||
680 | for title, value in templates: | |
681 | item = Gtk.MenuItem(title) | |
682 | item.connect( | |
683 | 'activate', self._on_tools_template_clicked, value) | |
684 | self._tools_menu.append(item) | |
685 | ||
686 | self._tools_menu.show_all() | |
687 | self._tools_menu.popup(None, None, None, None, 0, | |
688 | Gtk.get_current_event_time()) | |
689 | ||
690 | def _on_tools_template_clicked(self, item, value): | |
691 | self._set_value(value) | |
692 | ||
693 | def _tools_show_unused_toggled(self, widget=None): | |
694 | if widget: | |
695 | self._show_unused = widget.props.active | |
696 | self._state_column.props.visible = self._show_unused | |
697 | ||
698 | used = {row[Row.Name]: row | |
699 | for row in self._model if row[Row.Name] in SingleIndicators} | |
700 | if self._show_unused: | |
701 | for name in SingleIndicators - used.keys(): | |
702 | options = deepcopy(self.DefaultOptions[name]) | |
703 | options[Option.Name] = name | |
704 | with self._model.handler_block(self._on_row_changed_id),\ | |
705 | self._model.handler_block(self._on_row_inserted_id): | |
706 | rowiter = self._set_row(None, options, select=False) | |
707 | self._model[rowiter][Row.State] = False | |
708 | else: | |
709 | for row in used.values(): | |
710 | if row[Row.HasState] and not row[Row.State]: | |
711 | with self._model.handler_block(self._on_row_deleted_id): | |
712 | self._model.remove(row.iter) |
17 | 17 | |
18 | 18 | from gi.repository import Gtk |
19 | 19 | |
20 | from lightdm_gtk_greeter_settings.helpers import WidgetsWrapper | |
21 | from lightdm_gtk_greeter_settings.MultiheadSetupDialog import MultiheadSetupDialog | |
20 | 22 | from lightdm_gtk_greeter_settings.OptionEntry import BaseEntry |
21 | 23 | from lightdm_gtk_greeter_settings.OptionGroup import BaseGroup |
22 | from lightdm_gtk_greeter_settings.helpers import WidgetsWrapper | |
23 | from lightdm_gtk_greeter_settings.MultiheadSetupDialog import MultiheadSetupDialog | |
24 | 24 | |
25 | 25 | |
26 | 26 | __all__ = ['MonitorsGroup'] |
15 | 15 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 | 17 | |
18 | import os | |
19 | 18 | from builtins import max |
20 | 19 | from locale import gettext as _ |
21 | from gi.repository import Gtk, Gdk, GdkPixbuf | |
22 | ||
23 | from lightdm_gtk_greeter_settings import helpers | |
24 | from lightdm_gtk_greeter_settings.helpers import WidgetsWrapper, C_, ModelRowEnum | |
20 | ||
21 | from gi.repository import ( | |
22 | Gdk, | |
23 | GdkPixbuf, | |
24 | Gtk) | |
25 | ||
26 | from lightdm_gtk_greeter_settings.helpers import ( | |
27 | C_, | |
28 | check_path_accessibility, | |
29 | get_data_path, | |
30 | SimpleEnum, | |
31 | WidgetsEnum) | |
25 | 32 | |
26 | 33 | |
27 | 34 | __all__ = ['MultiheadSetupDialog'] |
28 | 35 | |
29 | 36 | |
30 | ROW = ModelRowEnum('NAME', 'BACKGROUND', | |
31 | 'USER_BG', 'USER_BG_DISABLED', | |
32 | 'LAPTOP', 'LAPTOP_DISABLED', | |
33 | 'BACKGROUND_PIXBUF', 'BACKGROUND_IS_COLOR', | |
34 | 'ERROR_VISIBLE', 'ERROR_TEXT') | |
35 | ||
36 | BG_ROW = ModelRowEnum('TEXT', 'TYPE') | |
37 | class Row(SimpleEnum): | |
38 | Name = () | |
39 | Background = () | |
40 | UserBg = () | |
41 | UserBgDisabled = () | |
42 | Laptop = () | |
43 | LaptopDisabled = () | |
44 | BackgroundPixbuf = () | |
45 | BackgroundIsColor = () | |
46 | ErrorVisible = () | |
47 | ErrorText = () | |
48 | ||
49 | ||
50 | class BackgroundRow(SimpleEnum): | |
51 | Text = () | |
52 | Type = () | |
37 | 53 | |
38 | 54 | |
39 | 55 | class MultiheadSetupDialog(Gtk.Dialog): |
40 | ||
41 | 56 | __gtype_name__ = 'MultiheadSetupDialog' |
42 | 57 | |
43 | 58 | def __new__(cls): |
44 | 59 | builder = Gtk.Builder() |
45 | builder.add_from_file(helpers.get_data_path('%s.ui' % cls.__name__)) | |
60 | builder.add_from_file(get_data_path('%s.ui' % cls.__name__)) | |
46 | 61 | window = builder.get_object('multihead_setup_dialog') |
47 | window._widgets = WidgetsWrapper(builder) | |
62 | window.builder = builder | |
48 | 63 | builder.connect_signals(window) |
49 | window._init_window() | |
64 | window.init_window() | |
50 | 65 | return window |
51 | 66 | |
52 | def _init_window(self): | |
67 | class Widgets(WidgetsEnum): | |
68 | treeview = 'monitors_treeview' | |
69 | model = 'monitors_model' | |
70 | selection = 'monitors_selection' | |
71 | bg_model = 'background_model' | |
72 | bg_renderer = 'bg_renderer' | |
73 | bg_column = 'background_column' | |
74 | name_column = 'name_column' | |
75 | remove = 'remove_button' | |
76 | available = 'monitors_label' | |
77 | ||
78 | builder = None | |
79 | ||
80 | def init_window(self): | |
81 | self._widgets = self.Widgets(builder=self.builder) | |
53 | 82 | self._group = None |
54 | 83 | self._available_monitors = None |
55 | 84 | |
56 | self._remove_button = self._widgets['remove_button'] | |
57 | self._treeview = self._widgets['monitors_treeview'] | |
58 | self._model = self._widgets['monitors_model'] | |
59 | self._bg_model = self._widgets['background_model'] | |
60 | self._selection = self._treeview.get_selection() | |
61 | self._bg_renderer = self._widgets['bg_renderer'] | |
62 | self._bg_column = self._widgets['background_column'] | |
63 | self._name_column = self._widgets['name_column'] | |
64 | 85 | self._file_dialog = None |
65 | 86 | self._color_dialog = None |
66 | 87 | self._invalid_name_dialog = None |
67 | 88 | self._name_exists_dialog = None |
68 | 89 | |
69 | self._treeview.props.tooltip_column = ROW.ERROR_TEXT | |
70 | self._bg_renderer.set_property('placeholder-text', | |
71 | C_('option|multihead', 'Use default value')) | |
90 | self._widgets.treeview.props.tooltip_column = Row.ErrorText | |
91 | self._widgets.bg_renderer.set_property('placeholder-text', | |
92 | C_('option|multihead', 'Use default value')) | |
72 | 93 | |
73 | 94 | def _update_monitors_label(self): |
74 | used = set(row[ROW.NAME] for row in self._model) | |
75 | monitors = ['<a href="{name}">{name}</a>'.format(name=name) | |
76 | for name in self._available_monitors if name not in used] | |
77 | label = C_('option|multihead', 'Available monitors: <i>{monitors}</i>')\ | |
78 | .format(monitors=', '.join(monitors or ('none',))) | |
79 | self._widgets['monitors_label'].props.label = label | |
80 | ||
95 | if not self._available_monitors: | |
96 | self._widgets.available.props.visible = False | |
97 | return | |
98 | ||
99 | used = set(row[Row.Name] for row in self._widgets.model) | |
100 | monitors = [] | |
101 | for name in self._available_monitors: | |
102 | if name in used: | |
103 | monitors.append(name) | |
104 | else: | |
105 | monitors.append('<i><a href="{name}">{name}</a></i>'.format(name=name)) | |
106 | ||
107 | label = C_('option|multihead', | |
108 | 'Available monitors: {monitors}').format(monitors=', '.join(monitors)) | |
109 | self._widgets.available.props.label = label | |
110 | self._widgets.available.props.visible = True | |
81 | 111 | |
82 | 112 | def set_model(self, values): |
83 | self._model.clear() | |
113 | self._widgets.model.clear() | |
84 | 114 | for name, entry in values.items(): |
85 | rowiter = self._model.append(ROW(NAME=name, | |
86 | BACKGROUND=entry['background'], | |
87 | USER_BG=entry['user-background'], | |
88 | USER_BG_DISABLED=entry['user-background'] is None, | |
89 | LAPTOP=entry['laptop'], | |
90 | LAPTOP_DISABLED=entry['laptop'] is None, | |
91 | BACKGROUND_PIXBUF=None, | |
92 | BACKGROUND_IS_COLOR=False, | |
93 | ERROR_VISIBLE=False, | |
94 | ERROR_TEXT=None)) | |
95 | self._update_row_appearance(rowiter) | |
115 | row = Row._make(Name=name, | |
116 | Background=entry['background'], | |
117 | UserBg=entry['user-background'], | |
118 | UserBgDisabled=entry['user-background'] is None, | |
119 | Laptop=entry['laptop'], | |
120 | LaptopDisabled=entry['laptop'] is None, | |
121 | BackgroundPixbuf=None, | |
122 | BackgroundIsColor=False, | |
123 | ErrorVisible=False, | |
124 | ErrorText=None) | |
125 | self._update_row_appearance(self._widgets.model.append(row)) | |
96 | 126 | screen = Gdk.Screen.get_default() |
97 | 127 | self._available_monitors = [screen.get_monitor_plug_name(i) |
98 | 128 | for i in range(screen.get_n_monitors())] |
99 | 129 | self._update_monitors_label() |
100 | 130 | |
101 | 131 | def get_model(self): |
102 | return {row[ROW.NAME]: | |
103 | { | |
104 | 'background': row[ROW.BACKGROUND], | |
105 | 'user-background': self._get_toggle_state(row, ROW.USER_BG, ROW.USER_BG_DISABLED), | |
106 | 'laptop': self._get_toggle_state(row, ROW.LAPTOP, ROW.LAPTOP_DISABLED) | |
107 | } | |
108 | for row in self._model} | |
132 | return { | |
133 | row[Row.Name]: | |
134 | { | |
135 | 'background': row[Row.Background], | |
136 | 'user-background': self._get_toggle_state(row, Row.UserBg, Row.UserBgDisabled), | |
137 | 'laptop': self._get_toggle_state(row, Row.Laptop, Row.LaptopDisabled) | |
138 | } | |
139 | for row in self._widgets.model} | |
109 | 140 | |
110 | 141 | def _update_row_appearance(self, rowiter): |
111 | row = self._model[rowiter] | |
112 | bg = row[ROW.BACKGROUND] | |
142 | row = self._widgets.model[rowiter] | |
143 | bg = row[Row.Background] | |
113 | 144 | |
114 | 145 | error = None |
115 | color = Gdk.color_parse(bg) | |
116 | if color: | |
117 | pixbuf = row[ROW.BACKGROUND_PIXBUF] | |
146 | color = Gdk.RGBA() | |
147 | if color.parse(bg): | |
148 | pixbuf = row[Row.BackgroundPixbuf] | |
118 | 149 | if not pixbuf: |
119 | pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, | |
120 | False, 8, 16, 16) | |
121 | row[ROW.BACKGROUND_PIXBUF] = pixbuf | |
122 | value = (int(0xFF / Gdk.Color.MAX_VALUE * color.red) << 24) + \ | |
123 | (int(0xFF / Gdk.Color.MAX_VALUE * color.green) << 16) + \ | |
124 | (int(0xFF / Gdk.Color.MAX_VALUE * color.blue) << 8) | |
150 | pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, False, 8, 16, 16) | |
151 | row[Row.BackgroundPixbuf] = pixbuf | |
152 | value = (int(0xFF * color.red) << 24) + \ | |
153 | (int(0xFF * color.green) << 16) + \ | |
154 | (int(0xFF * color.blue) << 8) | |
125 | 155 | pixbuf.fill(value) |
126 | row[ROW.BACKGROUND_IS_COLOR] = True | |
156 | row[Row.BackgroundIsColor] = True | |
127 | 157 | else: |
128 | row[ROW.BACKGROUND_IS_COLOR] = False | |
129 | if not os.path.exists(bg): | |
130 | error = C_('option|multihead', 'File not found: {path}'.format(path=bg)) | |
131 | else: | |
132 | try: | |
133 | if not helpers.file_is_readable_by_greeter(bg): | |
134 | error = C_('option|multihead', 'File may be not readable for greeter: {path}'.format(path=bg)) | |
135 | except: | |
136 | error = C_('option|multihead', 'Failed to check permissions for file: {path}'.format(path=bg)) | |
137 | ||
138 | row[ROW.ERROR_VISIBLE] = error is not None | |
139 | row[ROW.ERROR_TEXT] = error | |
140 | ||
141 | _TOGGLE_STATES = {None: True, False: None, True: False} | |
158 | row[Row.BackgroundIsColor] = False | |
159 | if bg: | |
160 | error = check_path_accessibility(bg) | |
161 | ||
162 | row[Row.ErrorVisible] = error is not None | |
163 | row[Row.ErrorText] = error | |
164 | ||
165 | ToggleStatesSeq = {None: True, False: None, True: False} | |
142 | 166 | |
143 | 167 | def _get_toggle_state(self, row, active_column, inconsistent_column): |
144 | 168 | return None if row[inconsistent_column] else row[active_column] |
145 | 169 | |
146 | 170 | def _toggle_state(self, row, active_column, inconsistent_column): |
147 | 171 | state = self._get_toggle_state(row, active_column, inconsistent_column) |
148 | row[active_column] = self._TOGGLE_STATES[state] | |
149 | row[inconsistent_column] = self._TOGGLE_STATES[state] is None | |
172 | row[active_column] = self.ToggleStatesSeq[state] | |
173 | row[inconsistent_column] = self.ToggleStatesSeq[state] is None | |
150 | 174 | |
151 | 175 | def on_monitors_add_clicked(self, button): |
152 | 176 | prefix = 'monitor' |
153 | numbers = (row[ROW.NAME][len(prefix):] | |
154 | for row in self._model if row[ROW.NAME].startswith(prefix)) | |
177 | numbers = (row[Row.Name][len(prefix):] | |
178 | for row in self._widgets.model if row[Row.Name].startswith(prefix)) | |
155 | 179 | try: |
156 | 180 | max_number = max(int(v) for v in numbers if v.isdigit()) |
157 | 181 | except ValueError: |
158 | 182 | max_number = 0 |
159 | rowiter = self._model.append(ROW(NAME='%s%d' % (prefix, max_number + 1), | |
160 | USER_BG=False, | |
161 | USER_BG_ENABLED=False, | |
162 | LAPTOP=True, | |
163 | LAPTOP_ENABLED=False, | |
164 | BACKGROUND='', | |
165 | BACKGROUND_PIXBUF=None, | |
166 | BACKGROUND_IS_COLOR=False, | |
167 | ERROR_VISIBLE=False, | |
168 | ERROR_TEXT=None)) | |
169 | self._treeview.set_cursor(self._model.get_path(rowiter), self._name_column, True) | |
183 | ||
184 | row = Row._make(Name='%s%d' % (prefix, max_number + 1), | |
185 | UserBg=False, | |
186 | UserBgDisabled=False, | |
187 | Laptop=True, | |
188 | LaptopDisabled=False, | |
189 | Background='', | |
190 | BackgroundPixbuf=None, | |
191 | BackgroundIsColor=False, | |
192 | ErrorVisible=False, | |
193 | ErrorText=None) | |
194 | rowiter = self._widgets.model.append(row) | |
195 | self._widgets.treeview.set_cursor(self._widgets.model.get_path(rowiter), | |
196 | self._widgets.name_column, True) | |
170 | 197 | |
171 | 198 | def on_monitors_remove_clicked(self, button): |
172 | model, rowiter = self._treeview.get_selection().get_selected() | |
199 | model, rowiter = self._widgets.selection.get_selected() | |
173 | 200 | model.remove(rowiter) |
201 | self._update_monitors_label() | |
174 | 202 | |
175 | 203 | def on_selection_changed(self, selection): |
176 | self._remove_button.props.sensitive = selection.get_selected()[1] is not None | |
204 | self._widgets.remove.props.sensitive = all(selection.get_selected()) | |
177 | 205 | |
178 | 206 | def on_monitors_label_activate_link(self, label, name): |
179 | rowiter = self._model.append(ROW(NAME=name, | |
180 | USER_BG=False, | |
181 | USER_BG_ENABLED=False, | |
182 | LAPTOP=True, | |
183 | LAPTOP_ENABLED=False, | |
184 | BACKGROUND='', | |
185 | BACKGROUND_PIXBUF=None, | |
186 | BACKGROUND_IS_COLOR=False)) | |
207 | row = Row._make(Name=name, | |
208 | UserBg=False, | |
209 | UserBgDisabled=False, | |
210 | Laptop=True, | |
211 | LaptopDisabled=False, | |
212 | Background='', | |
213 | BackgroundPixbuf=None, | |
214 | BackgroundIsColor=True, | |
215 | ErrorVisible=False, | |
216 | ErrorText=None) | |
217 | ||
218 | rowiter = self._widgets.model.append(row) | |
187 | 219 | self._update_row_appearance(rowiter) |
188 | self._treeview.get_selection().select_iter(rowiter) | |
220 | self._widgets.selection.select_iter(rowiter) | |
189 | 221 | self._update_monitors_label() |
190 | 222 | return True |
191 | 223 | |
193 | 225 | combobox.connect('format-entry-text', self.on_bg_combobox_format) |
194 | 226 | |
195 | 227 | def on_bg_combobox_format(self, combobox, path): |
196 | model, rowiter = self._selection.get_selected() | |
197 | item_type = combobox.props.model[path][BG_ROW.TYPE] | |
198 | value = model[rowiter][ROW.BACKGROUND] | |
228 | model, rowiter = self._widgets.selection.get_selected() | |
229 | item_type = combobox.props.model[path][BackgroundRow.Type] | |
230 | value = model[rowiter][Row.Background] | |
231 | ||
199 | 232 | if item_type == 'path': |
200 | 233 | if not self._file_dialog: |
201 | 234 | self._file_dialog = Gtk.FileChooserDialog( |
202 | parent=self, | |
203 | buttons=(_('_OK'), Gtk.ResponseType.OK, | |
204 | _('_Cancel'), Gtk.ResponseType.CANCEL), | |
205 | title=C_('option|multihead', 'Select background file')) | |
235 | parent=self, | |
236 | buttons=(_('_OK'), Gtk.ResponseType.OK, | |
237 | _('_Cancel'), Gtk.ResponseType.CANCEL), | |
238 | title=C_('option|multihead', 'Select background file')) | |
206 | 239 | self._file_dialog.props.filter = Gtk.FileFilter() |
207 | 240 | self._file_dialog.props.filter.add_mime_type('image/*') |
208 | 241 | if self._file_dialog.run() == Gtk.ResponseType.OK: |
216 | 249 | self._color_dialog.hide() |
217 | 250 | else: |
218 | 251 | value = '' |
252 | ||
219 | 253 | combobox.set_active(-1) |
220 | 254 | return value |
221 | 255 | |
222 | 256 | def on_bg_renderer_edited(self, renderer, path, new_text): |
223 | self._model[path][ROW.BACKGROUND] = new_text | |
224 | self._update_row_appearance(self._model.get_iter(path)) | |
257 | self._widgets.model[path][Row.Background] = new_text | |
258 | self._update_row_appearance(self._widgets.model.get_iter(path)) | |
225 | 259 | |
226 | 260 | def on_name_renderer_edited(self, renderer, path, new_name): |
227 | old_name = self._model[path][ROW.NAME] | |
261 | old_name = self._widgets.model[path][Row.Name] | |
228 | 262 | invalid_name = not new_name.strip() |
229 | name_in_use = new_name != old_name and any(new_name == row[ROW.NAME] for row in self._model) | |
263 | name_in_use = new_name != old_name and any(new_name == row[Row.Name] | |
264 | for row in self._widgets.model) | |
230 | 265 | if invalid_name or name_in_use: |
231 | 266 | if not self._invalid_name_dialog: |
232 | 267 | self._invalid_name_dialog = Gtk.MessageDialog(parent=self, |
233 | 268 | buttons=Gtk.ButtonsType.OK) |
234 | 269 | self._invalid_name_dialog.set_property('text', |
235 | C_('option|multihead', 'Invalid name: "{name}"') | |
236 | .format(name=new_name)) | |
270 | C_('option|multihead', | |
271 | 'Invalid name: "{name}"') | |
272 | .format(name=new_name)) | |
237 | 273 | if name_in_use: |
238 | 274 | message = C_('option|multihead', 'This name already in use.') |
239 | 275 | else: |
242 | 278 | self._invalid_name_dialog.run() |
243 | 279 | self._invalid_name_dialog.hide() |
244 | 280 | else: |
245 | self._model[path][ROW.NAME] = new_name | |
281 | self._widgets.model[path][Row.Name] = new_name | |
246 | 282 | self._update_monitors_label() |
247 | 283 | |
248 | 284 | def on_user_bg_renderer_toggled(self, renderer, path): |
249 | self._toggle_state(self._model[path], | |
250 | ROW.USER_BG, ROW.USER_BG_DISABLED) | |
285 | self._toggle_state(self._widgets.model[path], Row.UserBg, Row.UserBgDisabled) | |
251 | 286 | |
252 | 287 | def on_laptop_renderer_toggled(self, renderer, path): |
253 | self._toggle_state(self._model[path], | |
254 | ROW.LAPTOP, ROW.LAPTOP_DISABLED) | |
255 | ||
288 | self._toggle_state(self._widgets.model[path], Row.Laptop, Row.LaptopDisabled) |
14 | 14 | # You should have received a copy of the GNU General Public License along |
15 | 15 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 | from builtins import isinstance | |
18 | from collections import OrderedDict | |
19 | from locale import gettext as _ | |
17 | ||
20 | 18 | import os |
21 | 19 | import time |
22 | ||
23 | from gi.repository import Gtk, Gdk, GObject, GLib | |
20 | from locale import gettext as _ | |
21 | ||
22 | from gi.repository import ( | |
23 | Gdk, | |
24 | GLib, | |
25 | GObject, | |
26 | Gtk) | |
27 | from lightdm_gtk_greeter_settings import helpers | |
28 | from lightdm_gtk_greeter_settings.helpers import ( | |
29 | C_, | |
30 | bool2string, | |
31 | string2bool, SimpleEnum) | |
24 | 32 | from lightdm_gtk_greeter_settings.IconChooserDialog import IconChooserDialog |
25 | from lightdm_gtk_greeter_settings.IndicatorChooserDialog import IndicatorChooserDialog | |
26 | from lightdm_gtk_greeter_settings import helpers | |
27 | from lightdm_gtk_greeter_settings.helpers import C_ | |
28 | from lightdm_gtk_greeter_settings.helpers import ModelRowEnum | |
29 | from lightdm_gtk_greeter_settings.helpers import string2bool, bool2string | |
30 | ||
31 | ||
32 | __all__ = ['BaseEntry', 'BooleanEntry', 'InvertedBooleanEntry', | |
33 | 'StringEntry', 'StringPathEntry', 'ClockFormatEntry', | |
34 | 'BackgroundEntry', 'IconEntry', 'IndicatorsEntry', | |
35 | 'AdjustmentEntry', 'ChoiceEntry', 'AccessibilityStatesEntry'] | |
36 | ||
37 | ||
38 | class BuilderWrapper: | |
39 | ||
40 | def __init__(self, builder, base): | |
41 | self._builder = builder | |
42 | self._base = base | |
43 | ||
44 | def __getitem__(self, key): | |
45 | return self._builder.get_object('%s_%s' % (self._base, key)) | |
33 | ||
34 | ||
35 | __all__ = [ | |
36 | 'AccessibilityStatesEntry', | |
37 | 'AdjustmentEntry', | |
38 | 'BackgroundEntry', | |
39 | 'BaseEntry', | |
40 | 'BooleanEntry', | |
41 | 'ChoiceEntry', | |
42 | 'ClockFormatEntry', | |
43 | 'IconEntry', | |
44 | 'InvertedBooleanEntry', | |
45 | 'StringEntry', | |
46 | 'StringPathEntry'] | |
46 | 47 | |
47 | 48 | |
48 | 49 | class BaseEntry(GObject.GObject): |
51 | 52 | super().__init__() |
52 | 53 | self._widgets = widgets |
53 | 54 | self._use = widgets['use'] |
55 | self._widgets_to_disable = [] | |
54 | 56 | if self._use: |
55 | 57 | self._use.connect('notify::active', self._on_use_toggled) |
56 | self._widgets_to_disable = None | |
57 | 58 | self._error = widgets['error'] |
58 | 59 | |
59 | 60 | @property |
99 | 100 | pass |
100 | 101 | |
101 | 102 | @GObject.Signal(flags=GObject.SIGNAL_RUN_CLEANUP) |
102 | def get(self, value: str) -> str: # @IgnorePep8 | |
103 | def get(self, value: str) -> str: | |
103 | 104 | pass |
104 | 105 | |
105 | 106 | @GObject.Signal(flags=GObject.SIGNAL_RUN_CLEANUP) |
106 | def set(self, value: str) -> str: # @IgnorePep8 | |
107 | def set(self, value: str) -> str: | |
107 | 108 | pass |
108 | 109 | |
109 | 110 | def __repr__(self): |
121 | 122 | raise NotImplementedError(self.__class__) |
122 | 123 | |
123 | 124 | def _get_error(self): |
124 | raise NotImplementedError(self.__class__) | |
125 | if self._error: | |
126 | return self._error.props.tooltip_text | |
127 | return None | |
125 | 128 | |
126 | 129 | def _set_error(self, text): |
127 | 130 | if self._error: |
147 | 150 | super().__init__(widgets) |
148 | 151 | self._value = widgets['value'] |
149 | 152 | self._value.connect('notify::active', self._emit_changed) |
150 | self._widgets_to_disable = [self._value] | |
153 | self._widgets_to_disable.append(self._value) | |
151 | 154 | |
152 | 155 | def _get_value(self): |
153 | 156 | return bool2string(self._value.props.active) |
173 | 176 | def __init__(self, widgets): |
174 | 177 | super().__init__(widgets) |
175 | 178 | self._value = widgets['value'] |
176 | self._widgets_to_disable = [self._value] | |
179 | self._widgets_to_disable.append(self._value) | |
177 | 180 | if isinstance(self._value.props.parent, Gtk.ComboBox): |
178 | 181 | self._widgets_to_disable += [self._value.props.parent] |
179 | 182 | self._value.connect('changed', self._emit_changed) |
180 | ||
183 | ||
181 | 184 | def _get_value(self): |
182 | 185 | return self._value.props.text |
183 | 186 | |
186 | 189 | |
187 | 190 | |
188 | 191 | class StringPathEntry(BaseEntry): |
192 | ||
193 | class Row(helpers.SimpleEnum): | |
194 | Title = () | |
195 | Type = () | |
196 | ||
197 | class ItemType(SimpleEnum): | |
198 | Select = 'select-path' | |
199 | Value = 'value' | |
200 | Separator = 'separator' | |
189 | 201 | |
190 | 202 | def __init__(self, widgets): |
191 | 203 | super().__init__(widgets) |
194 | 206 | |
195 | 207 | self._combo = widgets['combo'] |
196 | 208 | self._entry = widgets['entry'] |
197 | self._widgets_to_disable = [self._combo] | |
209 | self._widgets_to_disable.append(self._combo) | |
210 | self._filters = () | |
198 | 211 | |
199 | 212 | self._entry.connect('changed', self._emit_changed) |
200 | 213 | self._combo.connect('format-entry-text', self._on_combobox_format) |
201 | 214 | |
202 | 215 | self._combo.set_row_separator_func(self._row_separator_callback, None) |
203 | 216 | |
217 | def add_filter(self, file_filter): | |
218 | if self._file_dialog: | |
219 | self._file_dialog.add_filter(file_filter) | |
220 | elif self._filters: | |
221 | self._filters.append(file_filter) | |
222 | else: | |
223 | self._filters = [file_filter] | |
224 | ||
204 | 225 | def _get_value(self): |
205 | 226 | return self._entry.props.text |
206 | 227 | |
208 | 229 | self._entry.props.text = value or '' |
209 | 230 | |
210 | 231 | def _row_separator_callback(self, model, rowiter, data): |
211 | return model[rowiter][0] == '-' | |
232 | return model[rowiter][self.Row.Type] == self.ItemType.Separator | |
212 | 233 | |
213 | 234 | def _on_combobox_format(self, combobox, path): |
214 | value = '' | |
235 | value = self._entry.props.text | |
215 | 236 | item_id = combobox.get_active_id() |
216 | if item_id == 'select-path': | |
237 | if item_id == self.ItemType.Select: | |
217 | 238 | if not self._file_dialog: |
218 | 239 | self._file_dialog = Gtk.FileChooserDialog( |
219 | parent=self._combo.get_toplevel(), | |
220 | buttons=(_('_OK'), Gtk.ResponseType.OK, | |
221 | _('_Cancel'), Gtk.ResponseType.CANCEL), | |
222 | title=C_('option|StringPathEntry', 'Select path')) | |
240 | parent=self._combo.get_toplevel(), | |
241 | buttons=(_('_OK'), Gtk.ResponseType.OK, | |
242 | _('_Cancel'), Gtk.ResponseType.CANCEL), | |
243 | title=C_('option|StringPathEntry', 'Select path')) | |
244 | for f in self._filters: | |
245 | self._file_dialog.add_filter(f) | |
223 | 246 | if self._file_dialog.run() == Gtk.ResponseType.OK: |
224 | 247 | value = self._file_dialog.get_filename() |
225 | else: | |
226 | value = combobox.get_active_text() | |
227 | 248 | self._file_dialog.hide() |
228 | elif item_id == 'value': | |
229 | value = combobox.props.model[path][0] | |
249 | elif item_id == self.ItemType.Value: | |
250 | value = combobox.props.model[path][self.Row.Title] | |
251 | ||
230 | 252 | combobox.set_active(-1) |
253 | combobox.grab_focus() | |
231 | 254 | return value |
232 | 255 | |
233 | 256 | |
237 | 260 | super().__init__(widgets) |
238 | 261 | self._value = widgets['adjustment'] |
239 | 262 | self._view = widgets['view'] |
240 | self._widgets_to_disable = [self._view] | |
263 | self._widgets_to_disable.append(self._view) | |
241 | 264 | self._value.connect('value-changed', self._emit_changed) |
242 | 265 | |
243 | 266 | def _get_value(self): |
255 | 278 | def __init__(self, widgets): |
256 | 279 | super().__init__(widgets) |
257 | 280 | self._value = widgets['value'] |
258 | self._widgets_to_disable = [self._value] | |
281 | self._widgets_to_disable.append(self._value) | |
259 | 282 | self._value.connect('changed', self._emit_changed) |
260 | 283 | |
261 | 284 | def _get_value(self): |
338 | 361 | def __init__(self, widgets): |
339 | 362 | super().__init__(widgets) |
340 | 363 | self._value = widgets['value'] |
364 | self._widgets_to_disable.append(self._value) | |
341 | 365 | self._value.connect('font-set', self._emit_changed) |
342 | 366 | |
343 | 367 | def _get_value(self): |
356 | 380 | self._image = widgets['image'] |
357 | 381 | self._icon_item = widgets['icon_item'] |
358 | 382 | self._path_item = widgets['path_item'] |
383 | self._widgets_to_disable.append(self._button) | |
359 | 384 | self._icon_dialog = None |
360 | 385 | self._path_dialog = None |
361 | 386 | |
412 | 437 | width, height = image.get_size_request() |
413 | 438 | if -1 in (width, height): |
414 | 439 | width, height = 64, 64 |
415 | pixbuf = helpers.new_pixbuf_from_file_scaled_down(path, width, height) | |
440 | pixbuf = helpers.pixbuf_from_file_scaled_down(path, width, height) | |
416 | 441 | image.set_from_pixbuf(pixbuf) |
417 | 442 | return True |
418 | 443 | except GLib.Error: |
423 | 448 | if not self._icon_dialog: |
424 | 449 | self._icon_dialog = IconChooserDialog() |
425 | 450 | self._icon_dialog.props.transient_for = self._image.get_toplevel() |
426 | if self._value.startswith('#'): | |
451 | if self._value and self._value.startswith('#'): | |
427 | 452 | self._icon_dialog.select_icon(self._value[1:]) |
428 | 453 | if self._icon_dialog.run() == Gtk.ResponseType.OK: |
429 | self._set_icon(self._icon_dialog.get_icon_name()) | |
454 | self._set_icon(self._icon_dialog.get_selected_icon()) | |
430 | 455 | self._icon_dialog.hide() |
431 | 456 | |
432 | 457 | def _on_select_path(self, item): |
443 | 468 | preview.props.pixel_size = preview_size |
444 | 469 | preview.set_size_request(preview_size, preview_size) |
445 | 470 | |
446 | self._path_dialog.select_filename(self._value) | |
471 | if self._value: | |
472 | self._path_dialog.select_filename(self._value) | |
447 | 473 | if self._path_dialog.run() == Gtk.ResponseType.OK: |
448 | 474 | self._set_path(self._path_dialog.get_filename()) |
449 | 475 | self._path_dialog.hide() |
452 | 478 | self._set_image_from_path(chooser.props.preview_widget, chooser.get_filename()) |
453 | 479 | |
454 | 480 | |
455 | class IndicatorsEntry(BaseEntry): | |
456 | ROW = ModelRowEnum('NAME', 'TOOLTIP', 'EDITABLE', 'HAS_STATE', 'STATE') | |
457 | NAMES_DELIMITER = ';' | |
458 | DEFAULT_TOOLTIPS = {'~spacer': C_('option-entry|indicators', 'Spacer'), | |
459 | '~separator': C_('option-entry|indicators', 'Separator')} | |
460 | ||
461 | def __init__(self, widgets): | |
462 | super().__init__(widgets) | |
463 | self._toolbar = widgets['toolbar'] | |
464 | self._treeview = widgets['treeview'] | |
465 | self._selection = widgets['selection'] | |
466 | self._state_renderer = widgets['state_renderer'] | |
467 | self._name_column = widgets['name_column'] | |
468 | self._name_renderer = widgets['name_renderer'] | |
469 | self._add = widgets['add'] | |
470 | self._remove = widgets['remove'] | |
471 | self._up = widgets['up'] | |
472 | self._down = widgets['down'] | |
473 | self._model = widgets['model'] | |
474 | self._widgets_to_disable = [self._treeview, self._toolbar] | |
475 | self._indicators_dialog = None | |
476 | self._initial_items = OrderedDict((item.NAME, item) | |
477 | for item in map(self.ROW, self._model)) | |
478 | ||
479 | self._treeview.connect('key-press-event', self._on_key_press) | |
480 | self._selection.connect('changed', self._on_selection_changed) | |
481 | self._state_renderer.connect('toggled', self._on_state_toggled) | |
482 | self._name_renderer.connect('edited', self._on_name_edited) | |
483 | self._add.connect('clicked', self._on_add) | |
484 | self._remove.connect('clicked', self._on_remove) | |
485 | self._up.connect('clicked', self._on_up) | |
486 | self._down.connect('clicked', self._on_down) | |
487 | ||
488 | self._model.connect('row-changed', self._on_model_changed) | |
489 | self._model.connect('row-deleted', self._on_model_changed) | |
490 | self._model.connect('row-inserted', self._on_model_changed) | |
491 | self._model.connect('rows-reordered', self._on_model_changed) | |
492 | ||
493 | def _on_model_changed(self, *unused): | |
494 | self._emit_changed() | |
495 | ||
496 | def _get_value(self): | |
497 | names = (row[self.ROW.NAME] for row in self._model | |
498 | if not row[self.ROW.HAS_STATE] or row[self.ROW.STATE]) | |
499 | return self.NAMES_DELIMITER.join(names) | |
500 | ||
501 | def _set_value(self, value): | |
502 | self._model.clear() | |
503 | last_options = self._initial_items.copy() | |
504 | if value: | |
505 | for name in value.split(self.NAMES_DELIMITER): | |
506 | try: | |
507 | self._model[self._model.append(last_options.pop(name))]\ | |
508 | [self.ROW.STATE] = True | |
509 | except KeyError: | |
510 | self._model.append(self._get_indicator_tuple(name)) | |
511 | ||
512 | for item in list(last_options.values()): | |
513 | self._model.append(item) | |
514 | ||
515 | self._selection.select_path(0) | |
516 | ||
517 | def _remove_selection(self): | |
518 | model, rowiter = self._selection.get_selected() | |
519 | if rowiter: | |
520 | previter = model.iter_previous(rowiter) | |
521 | model.remove(rowiter) | |
522 | if previter: | |
523 | self._selection.select_iter(previter) | |
524 | ||
525 | def _move_selection(self, move_up): | |
526 | model, rowiter = self._selection.get_selected() | |
527 | if rowiter: | |
528 | if move_up: | |
529 | model.swap(rowiter, model.iter_previous(rowiter)) | |
530 | else: | |
531 | model.swap(rowiter, model.iter_next(rowiter)) | |
532 | self._on_selection_changed(self._selection) | |
533 | ||
534 | def _check_indicator(self, name): | |
535 | ''' Returns True if name is valid, error message or False otherwise ''' | |
536 | if not name: | |
537 | return False | |
538 | elif name not in ('~spacer', '~separator'): | |
539 | if any(row[self.ROW.NAME] == name for row in self._model): | |
540 | return C_('option-entry|indicators', | |
541 | 'Indicator "{indicator}" is already in the list')\ | |
542 | .format(indicator=name) | |
543 | return True | |
544 | ||
545 | def _add_indicator(self, name): | |
546 | if name: | |
547 | rowiter = self._model.append(self._get_indicator_tuple(name)) | |
548 | self._selection.select_iter(rowiter) | |
549 | self._treeview.grab_focus() | |
550 | ||
551 | def _get_indicator_tuple(self, name): | |
552 | tooltip = self.DEFAULT_TOOLTIPS.get(name, | |
553 | C_('option-entry|indicators', 'Indicator: {name}') | |
554 | .format(name=name)) | |
555 | editable = name not in ('~spacer', '~separator') | |
556 | return self.ROW(NAME=name, TOOLTIP=tooltip, EDITABLE=editable, | |
557 | HAS_STATE=False, STATE=False) | |
558 | ||
559 | def _on_key_press(self, treeview, event): | |
560 | if Gdk.keyval_name(event.keyval) == 'Delete': | |
561 | self._remove_selection() | |
562 | elif Gdk.keyval_name(event.keyval) == 'F2': | |
563 | model, rowiter = self._selection.get_selected() | |
564 | if rowiter and model[rowiter][self.COLUMN.EDITABLE]: | |
565 | self._treeview.set_cursor( | |
566 | model.get_path(rowiter), self.COLUMN.NAME, True) | |
567 | else: | |
568 | return False | |
569 | return True | |
570 | ||
571 | def _on_state_toggled(self, renderer, path): | |
572 | self._model[path][self.ROW.STATE] = not self._model[path][self.ROW.STATE] | |
573 | ||
574 | def _on_name_edited(self, renderer, path, name): | |
575 | check = self._check_indicator(name) | |
576 | if not isinstance(check, str) and check: | |
577 | self._model[path][self.ROW.NAME] = name | |
578 | ||
579 | def _on_selection_changed(self, selection): | |
580 | model, rowiter = selection.get_selected() | |
581 | has_selection = rowiter is not None | |
582 | self._remove.props.sensitive = has_selection and \ | |
583 | not model[rowiter][self.ROW.HAS_STATE] | |
584 | self._down.props.sensitive = has_selection and model.iter_next( | |
585 | rowiter) is not None | |
586 | self._up.props.sensitive = has_selection and model.iter_previous( | |
587 | rowiter) is not None | |
588 | if has_selection: | |
589 | self._treeview.scroll_to_cell(model.get_path(rowiter)) | |
590 | ||
591 | def _on_add(self, *args): | |
592 | if not self._indicators_dialog: | |
593 | self._indicators_dialog = IndicatorChooserDialog( | |
594 | check_callback=self._check_indicator, | |
595 | add_callback=self._add_indicator) | |
596 | self._indicators_dialog.props.transient_for = \ | |
597 | self._treeview.get_toplevel() | |
598 | name = self._indicators_dialog.get_indicator() | |
599 | if name: | |
600 | self._add_indicator(name) | |
601 | ||
602 | def _on_remove(self, *args): | |
603 | self._remove_selection() | |
604 | ||
605 | def _on_up(self, *args): | |
606 | self._move_selection(move_up=True) | |
607 | ||
608 | def _on_down(self, *args): | |
609 | self._move_selection(move_up=False) | |
610 | ||
611 | ||
612 | 481 | class AccessibilityStatesEntry(BaseEntry): |
613 | 482 | |
614 | OPTIONS = {'keyboard', 'reader', 'contrast', 'font'} | |
615 | ||
616 | def __init__(self, widgets): | |
617 | super().__init__(widgets) | |
618 | ||
619 | self._states = {name: widgets[name] for name in self.OPTIONS} | |
483 | Options = {'keyboard', 'reader', 'contrast', 'font'} | |
484 | ||
485 | def __init__(self, widgets): | |
486 | super().__init__(widgets) | |
487 | ||
488 | self._states = {name: widgets[name] for name in self.Options} | |
620 | 489 | |
621 | 490 | for w in self._states.values(): |
622 | 491 | w.connect('changed', self._emit_changed) |
623 | 492 | |
624 | 493 | def _get_value(self): |
625 | states = {name: widget.props.active_id for (name, widget) in self._states.items()} | |
494 | states = {name: widget.props.active_id | |
495 | for (name, widget) in self._states.items()} | |
626 | 496 | return ';'.join(state + name |
627 | 497 | for (name, state) in states.items() if state not in {None, '-'}) |
628 | 498 | |
632 | 502 | for v in value.split(';') if v) |
633 | 503 | else: |
634 | 504 | states = {} |
635 | for name in self.OPTIONS: | |
505 | for name in self.Options: | |
636 | 506 | self._states[name].props.active_id = states.get(name, '-') |
637 |
16 | 16 | |
17 | 17 | |
18 | 18 | from gi.repository import GObject |
19 | ||
20 | from lightdm_gtk_greeter_settings.helpers import WidgetsWrapper | |
19 | 21 | from lightdm_gtk_greeter_settings.OptionEntry import BaseEntry |
20 | from lightdm_gtk_greeter_settings.helpers import WidgetsWrapper | |
21 | 22 | |
22 | 23 | |
23 | __all__ = ['BaseGroup', 'SimpleGroup'] | |
24 | __all__ = [ | |
25 | 'BaseGroup', | |
26 | 'SimpleGroup'] | |
24 | 27 | |
25 | 28 | |
26 | 29 | # Broken solution - too complex |
27 | ||
28 | 30 | class BaseGroup(GObject.GObject): |
29 | 31 | |
30 | 32 | def __init__(self, widgets): |
88 | 90 | config.set(self._name, key, entry.value) |
89 | 91 | else: |
90 | 92 | config.remove_option(self._name, key) |
91 |
14 | 14 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
15 | 15 | |
16 | 16 | |
17 | from itertools import product | |
18 | ||
17 | 19 | from gi.repository import Gtk |
18 | from itertools import product | |
20 | ||
21 | from lightdm_gtk_greeter_settings.helpers import WidgetsWrapper | |
19 | 22 | from lightdm_gtk_greeter_settings.OptionEntry import BaseEntry |
20 | from lightdm_gtk_greeter_settings.helpers import WidgetsWrapper | |
21 | 23 | |
22 | 24 | |
23 | 25 | __all__ = ['PositionEntry'] |
26 | 28 | class PositionEntry(BaseEntry): |
27 | 29 | |
28 | 30 | class Dimension: |
31 | ||
29 | 32 | def __init__(self, widgets, on_changed): |
30 | 33 | self._entry = widgets['entry'] |
31 | 34 | self._percents = widgets['percents'] |
104 | 107 | def _on_value_changed(self, widget): |
105 | 108 | self._on_changed(self) |
106 | 109 | |
107 | ||
108 | 110 | ASSUMED_WINDOW_SIZE = 430, 240 |
109 | 111 | |
110 | 112 | def __init__(self, widgets): |
128 | 130 | for (left, x_anchor), (top, y_anchor), w in anchors: |
129 | 131 | w.props.halign = anchors_align[left] |
130 | 132 | w.props.valign = anchors_align[top] |
131 | w.props.group = anchors[0][-1] | |
133 | if w != anchors[0][-1]: | |
134 | w.props.group = anchors[0][-1] | |
132 | 135 | w.connect('toggled', self._on_anchor_toggled, x_anchor, y_anchor) |
133 | 136 | self._grid.attach(w, left, top, 1, 1) |
134 | 137 | self._anchors[x_anchor, y_anchor] = w |
140 | 143 | |
141 | 144 | self._on_gdk_screen_changed() |
142 | 145 | |
143 | self._screen_overlay.connect('get-child-position', self._on_screen_overlay_get_child_position) | |
146 | self._screen_overlay.connect( | |
147 | 'get-child-position', self._on_screen_overlay_get_child_position) | |
144 | 148 | self._screen_overlay.connect('screen-changed', self._on_gdk_screen_changed) |
145 | 149 | |
146 | 150 | def _get_value(self): |
178 | 182 | def _on_screen_overlay_get_child_position(self, overlay, child, allocation): |
179 | 183 | screen = overlay.get_allocation() |
180 | 184 | |
181 | if self._last_window_allocation and self._last_overlay_size == (screen.width, screen.height): | |
182 | allocation.x, allocation.y, allocation.width, allocation.height = self._last_window_allocation | |
185 | if self._last_window_allocation and \ | |
186 | self._last_overlay_size == (screen.width, screen.height): | |
187 | (allocation.x, allocation.y, | |
188 | allocation.width, allocation.height) = self._last_window_allocation | |
183 | 189 | return True |
184 | 190 | self._last_overlay_size = screen.width, screen.height |
185 | 191 | |
193 | 199 | # And check what actually we have now |
194 | 200 | width, height = child.size_request().width, child.size_request().height |
195 | 201 | |
196 | x = self._get_corrected_position(int(self._x.get_value_for_screen(self._screen_size[0]) * scale), | |
197 | screen.width, width, self._x.anchor) | |
198 | y = self._get_corrected_position(int(self._y.get_value_for_screen(self._screen_size[1]) * scale), | |
199 | screen.height, height, self._y.anchor) | |
202 | x = int(self._x.get_value_for_screen(self._screen_size[0]) * scale) | |
203 | y = int(self._y.get_value_for_screen(self._screen_size[1]) * scale) | |
204 | ||
205 | x = self._get_corrected_position(x, screen.width, width, self._x.anchor) | |
206 | y = self._get_corrected_position(y, screen.height, height, self._y.anchor) | |
200 | 207 | |
201 | 208 | self._last_window_allocation = x, y, width, height |
202 | 209 | allocation.x, allocation.y, allocation.width, allocation.height = x, y, width, height |
212 | 219 | self._screen_overlay.queue_resize() |
213 | 220 | self._emit_changed() |
214 | 221 | |
215 | def _on_gdk_screen_changed(self, widget = None, prev_screen = None): | |
222 | def _on_gdk_screen_changed(self, widget=None, prev_screen=None): | |
216 | 223 | screen = self._screen_overlay.get_toplevel().get_screen() |
217 | 224 | geometry = screen.get_monitor_geometry(screen.get_primary_monitor()) |
218 | 225 | self._screen_size = geometry.width, geometry.height |
219 | 226 | self._screen_frame.props.ratio = geometry.width / geometry.height |
220 | ||
221 |
13 | 13 | # |
14 | 14 | # You should have received a copy of the GNU General Public License along |
15 | 15 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | ||
16 | 17 | |
17 | 18 | def main(): |
18 | 19 | from gi.repository import Gtk |
14 | 14 | # You should have received a copy of the GNU General Public License along |
15 | 15 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 | from collections import namedtuple | |
18 | from itertools import chain | |
17 | ||
19 | 18 | import configparser |
20 | 19 | import glob |
21 | 20 | import locale |
21 | import os | |
22 | 22 | import pwd |
23 | import os | |
24 | 23 | import stat |
25 | 24 | |
26 | from gi.repository import Gtk, GdkPixbuf | |
25 | from collections import ( | |
26 | namedtuple, | |
27 | OrderedDict, | |
28 | defaultdict) | |
29 | from itertools import ( | |
30 | chain, | |
31 | accumulate) | |
32 | from locale import gettext as _ | |
33 | ||
34 | from gi.repository import ( | |
35 | GdkPixbuf, | |
36 | GLib, | |
37 | GObject, | |
38 | Gtk, | |
39 | Pango) | |
27 | 40 | |
28 | 41 | |
29 | 42 | __license__ = 'GPL-3' |
33 | 46 | |
34 | 47 | |
35 | 48 | try: |
36 | from . installation_config import * | |
49 | from . installation_config import ( | |
50 | __version__, | |
51 | __data_directory__, | |
52 | __config_path__) | |
37 | 53 | except ImportError: |
38 | 54 | pass |
39 | 55 | |
40 | 56 | |
41 | __all__ = ['C_', 'NC_', | |
42 | 'get_data_path', 'get_config_path', 'show_message', | |
43 | 'bool2string', 'string2bool', 'new_pixbuf_from_file_scaled_down', | |
44 | 'file_is_readable_by_greeter', | |
45 | 'ModelRowEnum', 'WidgetsWrapper'] | |
57 | __all__ = [ | |
58 | 'bool2string', | |
59 | 'C_', | |
60 | 'check_path_accessibility', | |
61 | 'DefaultValueDict', | |
62 | 'file_is_readable_by_greeter', | |
63 | 'get_config_path', | |
64 | 'get_data_path', | |
65 | 'get_markup_error', | |
66 | 'get_version', | |
67 | 'ModelRowEnum', | |
68 | 'NC_', | |
69 | 'pixbuf_from_file_scaled_down', | |
70 | 'show_message', | |
71 | 'SimpleEnum', | |
72 | 'string2bool', | |
73 | 'TreeStoreDataWrapper', | |
74 | 'WidgetsEnum', | |
75 | 'WidgetsWrapper'] | |
46 | 76 | |
47 | 77 | |
48 | 78 | def C_(context, message): |
49 | CONTEXT_SEPARATOR = '\x04' | |
50 | message_with_context = '{}{}{}'.format(context, CONTEXT_SEPARATOR, message) | |
79 | separator = '\x04' | |
80 | message_with_context = '{}{}{}'.format(context, separator, message) | |
51 | 81 | result = locale.gettext(message_with_context) |
52 | if CONTEXT_SEPARATOR in result: | |
82 | if separator in result: | |
53 | 83 | result = message |
54 | 84 | return result |
55 | 85 | |
67 | 97 | return os.path.abspath(__config_path__) |
68 | 98 | |
69 | 99 | |
100 | def get_version(): | |
101 | return __version__ | |
102 | ||
103 | ||
104 | def bool2string(value): | |
105 | return 'true' if value else 'false' | |
106 | ||
107 | ||
108 | def string2bool(value): | |
109 | return value and value.lower() in ('true', 'yes', '1') | |
110 | ||
111 | ||
70 | 112 | def show_message(**kwargs): |
71 | dialog = Gtk.MessageDialog(parent=Gtk.Window.list_toplevels()[0], buttons=Gtk.ButtonsType.CLOSE, **kwargs) | |
113 | dialog = Gtk.MessageDialog(parent=Gtk.Window.list_toplevels()[0], | |
114 | buttons=Gtk.ButtonsType.CLOSE, **kwargs) | |
72 | 115 | dialog.run() |
73 | 116 | dialog.destroy() |
74 | 117 | |
75 | def get_version(): | |
76 | return __version__ | |
77 | ||
78 | ||
79 | def bool2string(value): | |
80 | return 'true' if value else 'false' | |
81 | ||
82 | ||
83 | def string2bool(value): | |
84 | return value and value.lower() in ('true', 'yes', '1') | |
85 | ||
86 | ||
87 | def new_pixbuf_from_file_scaled_down(path: str, width: int, height: int): | |
118 | ||
119 | def pixbuf_from_file_scaled_down(path, width, height): | |
88 | 120 | pixbuf = GdkPixbuf.Pixbuf.new_from_file(path) |
89 | 121 | scale = max(pixbuf.props.width / width, pixbuf.props.height / height) |
90 | 122 | |
96 | 128 | return pixbuf |
97 | 129 | |
98 | 130 | |
99 | def file_is_readable_by_greeter(path): | |
131 | def check_path_accessibility(path): | |
132 | """Return None if file is readable by greeter and error message otherwise""" | |
133 | ||
134 | if not os.path.exists(path): | |
135 | return _('File not found: {path}').format(path=path) | |
136 | ||
100 | 137 | try: |
101 | uid, groups = file_is_readable_by_greeter.id_cached_data | |
138 | uid, gids = check_path_accessibility.id_cached_data | |
102 | 139 | except AttributeError: |
103 | 140 | files = glob.glob('/etc/lightdm/lightdm.d/*.conf') |
104 | 141 | files += ['/etc/lightdm/lightdm.conf'] |
108 | 145 | |
109 | 146 | pw = pwd.getpwnam(username) |
110 | 147 | uid = pw.pw_uid |
111 | groups = set(os.getgrouplist(username, pw.pw_gid)) | |
112 | file_is_readable_by_greeter.id_cached_data = uid, groups | |
148 | gids = set(os.getgrouplist(username, pw.pw_gid)) | |
149 | check_path_accessibility.id_cached_data = uid, gids | |
113 | 150 | |
114 | 151 | parts = os.path.normpath(path).split(os.path.sep) |
115 | 152 | if not parts[0]: |
116 | 153 | parts[0] = os.path.sep |
117 | 154 | |
118 | def readable(st, uid, gids): | |
155 | def check(p): | |
156 | try: | |
157 | st = os.stat(p) | |
158 | except OSError as e: | |
159 | return _('Failed to check permissions: {error}'.format(error=e.strerror)) | |
160 | ||
119 | 161 | if stat.S_ISDIR(st.st_mode) and not stat.S_IREAD: |
120 | return False | |
162 | return _('Directory is not readable: {path}'.format(path=p)) | |
121 | 163 | if st.st_uid == uid: |
122 | return bool(st.st_mode & stat.S_IRUSR) | |
123 | if st.st_gid in groups: | |
124 | return bool(st.st_mode & stat.S_IRGRP) | |
125 | return bool(st.st_mode & stat.S_IROTH) | |
126 | ||
127 | return all(readable(os.stat(os.path.join(*parts[:i+1])), uid, groups) | |
128 | for i in range(len(parts))) | |
129 | ||
130 | ||
131 | class ModelRowEnum: | |
132 | ||
133 | def __init__(self, *names): | |
134 | self.__keys = tuple(names) | |
135 | self.__values = {name: i for i, name in enumerate(names)} | |
136 | self.__dict__.update(self.__values) | |
137 | self.__RowTuple = namedtuple('ModelRowEnumTuple', names) | |
138 | ||
139 | def __call__(self, *args, **kwargs): | |
164 | return not (st.st_mode & stat.S_IRUSR) and \ | |
165 | _('LighDM do not have permissions to read path: {path}'.format(path=p)) | |
166 | if st.st_gid in gids: | |
167 | return not (st.st_mode & stat.S_IRGRP) and \ | |
168 | _('LighDM do not have permissions to read path: {path}'.format(path=p)) | |
169 | return not (st.st_mode & stat.S_IROTH) and \ | |
170 | _('LighDM do not have permissions to read path: {path}'.format(path=p)) | |
171 | ||
172 | errors = (check(p) for p in accumulate(parts, os.path.join)) | |
173 | return next((error for error in errors if error), None) | |
174 | ||
175 | ||
176 | def get_markup_error(markup): | |
177 | try: | |
178 | Pango.parse_markup(markup, -1, '\0') | |
179 | except GLib.Error as e: | |
180 | return e.message | |
181 | return None | |
182 | ||
183 | ||
184 | class DefaultValueDict(defaultdict): | |
185 | ||
186 | def __init__(self, *items, default=None, factory=None, source=None): | |
187 | super().__init__(None, source or items) | |
188 | self._value = default | |
189 | self._factory = factory | |
190 | ||
191 | def __missing__(self, key): | |
192 | return self._factory(key) if self._factory else self._value | |
193 | ||
194 | ||
195 | class SimpleEnumMeta(type): | |
196 | ||
197 | @classmethod | |
198 | def __prepare__(mcs, *args, **kwargs): | |
199 | return OrderedDict() | |
200 | ||
201 | def __new__(self, cls, bases, classdict): | |
202 | obj = super().__new__(self, cls, bases, classdict) | |
203 | obj._dict = OrderedDict((k, v) | |
204 | for k, v in classdict.items() if obj._accept_member_(k, v)) | |
205 | obj._tuple_type = namedtuple(obj.__class__.__name__ + 'Tuple', obj._dict.keys()) | |
206 | keys = list(obj._dict.keys()) | |
207 | for i in range(len(keys)): | |
208 | if obj._dict[keys[i]] is (): | |
209 | v = 0 if i == 0 else obj._dict[keys[i - 1]] + 1 | |
210 | setattr(obj, keys[i], v) | |
211 | obj._dict[keys[i]] = v | |
212 | return obj | |
213 | ||
214 | def __contains__(self, value): | |
215 | return value in self._dict.values() | |
216 | ||
217 | def __iter__(self): | |
218 | return iter(self._dict.values()) | |
219 | ||
220 | def _make(self, *args, **kwargs): | |
221 | return self._tuple_type._make(self._imake(*args, **kwargs)) | |
222 | ||
223 | def _imake(self, *args, **kwargs): | |
140 | 224 | if args: |
141 | return self.__RowTuple._make(chain.from_iterable(args)) | |
225 | return args | |
226 | elif kwargs: | |
227 | return (kwargs.get(k, v) for k, v in self._dict.items()) | |
142 | 228 | else: |
143 | return self.__RowTuple._make(kwargs.get(name, i) for i, name in enumerate(self.__keys)) | |
229 | return self._dict.values() | |
230 | ||
231 | ||
232 | class SimpleEnum(metaclass=SimpleEnumMeta): | |
233 | _dict = None | |
234 | ||
235 | def __init__(self, *args, **kwargs): | |
236 | if kwargs: | |
237 | self.__dict__.update(kwargs) | |
238 | else: | |
239 | self.__dict__.update((k, args[i]) for i, k in enumerate(self._dict)) | |
240 | ||
241 | def __iter__(self): | |
242 | return (self.__dict__[k] for k in self._dict) | |
243 | ||
244 | @classmethod | |
245 | def _accept_member_(cls, name, value): | |
246 | return not name.startswith('_') and not name.endswith('_') | |
247 | ||
248 | ||
249 | class WidgetsEnum(SimpleEnum): | |
250 | ||
251 | def __init__(self, wrapper=None, builder=None): | |
252 | getter = wrapper.__getitem__ if wrapper else builder.get_object | |
253 | for k, v in self._dict.items(): | |
254 | if isinstance(v, type) and issubclass(v, WidgetsEnum): | |
255 | self.__dict__[k] = v(WidgetsWrapper(wrapper or builder, k)) | |
256 | else: | |
257 | self.__dict__[k] = getter(v or k) | |
144 | 258 | |
145 | 259 | |
146 | 260 | class WidgetsWrapper: |
261 | _builder = None | |
262 | _prefixes = None | |
147 | 263 | |
148 | 264 | def __init__(self, source, *prefixes): |
149 | 265 | if isinstance(source, Gtk.Builder): |
159 | 275 | if not isinstance(args, tuple): |
160 | 276 | args = (args,) |
161 | 277 | return self._builder.get_object('_'.join(chain(self._prefixes, args))) |
278 | ||
279 | ||
280 | class TreeStoreDataWrapper(GObject.Object): | |
281 | ||
282 | def __init__(self, data): | |
283 | super().__init__() | |
284 | self.data = data |