1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2018 The gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 # gpodder.gtkui.config -- Config object with GTK+ support (2009-08-24)
27 import gi
# isort:skip
28 gi
.require_version('Gdk', '3.0') # isort:skip
29 gi
.require_version('Gtk', '3.0') # isort:skip
30 from gi
.repository
import Gdk
, Gtk
, Pango
33 from gpodder
import config
, util
35 logger
= logging
.getLogger(__name__
)
40 class ConfigModel(Gtk
.ListStore
):
41 C_NAME
, C_TYPE_TEXT
, C_VALUE_TEXT
, C_TYPE
, C_EDITABLE
, C_FONT_STYLE
, \
42 C_IS_BOOLEAN
, C_BOOLEAN_VALUE
= list(range(8))
44 def __init__(self
, config
):
45 Gtk
.ListStore
.__init
__(self
, str, str, str, object, bool, int, bool, bool)
50 self
._config
.add_observer(self
._on
_update
)
52 def _type_as_string(self
, type):
62 def _fill_model(self
):
64 for key
in sorted(self
._config
.all_keys()):
65 # Ignore Gtk window state data (position, size, ...)
66 if key
.startswith('ui.gtk.state.'):
69 value
= self
._config
._lookup
(key
)
70 fieldtype
= type(value
)
72 style
= Pango
.Style
.NORMAL
73 # if value == default:
74 # style = Pango.Style.NORMAL
76 # style = Pango.Style.ITALIC
78 self
.append((key
, self
._type
_as
_string
(fieldtype
),
79 config
.config_value_to_string(value
),
80 fieldtype
, fieldtype
is not bool, style
,
81 fieldtype
is bool, bool(value
)))
83 def _on_update(self
, name
, old_value
, new_value
):
85 if row
[self
.C_NAME
] == name
:
86 style
= Pango
.Style
.NORMAL
87 # if new_value == self._config.Settings[name]:
88 # style = Pango.Style.NORMAL
90 # style = Pango.Style.ITALIC
91 new_value_text
= config
.config_value_to_string(new_value
)
93 self
.C_VALUE_TEXT
, new_value_text
,
94 self
.C_BOOLEAN_VALUE
, bool(new_value
),
95 self
.C_FONT_STYLE
, style
)
98 def stop_observing(self
):
99 self
._config
.remove_observer(self
._on
_update
)
102 class UIConfig(config
.Config
):
103 def __init__(self
, filename
='gpodder.conf'):
104 config
.Config
.__init
__(self
, filename
)
105 self
.__ignore
_window
_events
= False
107 def connect_gtk_editable(self
, name
, editable
):
108 editable
.delete_text(0, -1)
109 editable
.insert_text(str(getattr(self
, name
)))
111 def _editable_changed(editable
):
112 setattr(self
, name
, editable
.get_chars(0, -1))
113 editable
.connect('changed', _editable_changed
)
115 def connect_gtk_spinbutton(self
, name
, spinbutton
, forced_upper
=None):
117 bind a Gtk.SpinButton to a configuration entry.
119 It's now possible to specify an upper value (forced_upper).
120 It's not done automatically (always look for name + '_max') because it's
121 used only once. If it becomes commonplace, better make it automatic.
123 :param str name: configuration key (e.g. 'limit.downloads.concurrent')
124 :param Gtk.SpinButton spinbutton: button to bind to config
125 :param float forced_upper: forced upper limit on spinbutton.
126 Overrides value in .ui to be consistent with code
128 current_value
= getattr(self
, name
)
130 adjustment
= spinbutton
.get_adjustment()
131 if forced_upper
is not None:
132 adjustment
.set_upper(forced_upper
)
133 if current_value
> adjustment
.get_upper():
134 adjustment
.set_upper(current_value
)
136 spinbutton
.set_value(current_value
)
138 def _spinbutton_changed(spinbutton
):
139 setattr(self
, name
, spinbutton
.get_value())
140 spinbutton
.connect('value-changed', _spinbutton_changed
)
142 def connect_gtk_paned(self
, name
, paned
):
143 paned
.set_position(getattr(self
, name
))
144 paned_child
= paned
.get_child1()
146 def _child_size_allocate(x
, y
):
147 setattr(self
, name
, paned
.get_position())
148 paned_child
.connect('size-allocate', _child_size_allocate
)
150 def connect_gtk_togglebutton(self
, name
, togglebutton
):
151 togglebutton
.set_active(getattr(self
, name
))
153 def _togglebutton_toggled(togglebutton
):
154 setattr(self
, name
, togglebutton
.get_active())
155 togglebutton
.connect('toggled', _togglebutton_toggled
)
157 def connect_gtk_window(self
, window
, config_prefix
, show_window
=False):
158 cfg
= getattr(self
.ui
.gtk
.state
, config_prefix
)
161 window
.set_gravity(Gdk
.Gravity
.STATIC
)
163 if -1 not in (cfg
.x
, cfg
.y
, cfg
.width
, cfg
.height
):
164 # get screen resolution
165 screen
= Gdk
.Screen
.get_default()
168 for i
in range(0, screen
.get_n_monitors()):
169 monitor
= screen
.get_monitor_geometry(i
)
170 screen_width
+= monitor
.width
171 screen_height
+= monitor
.height
172 logger
.debug('Screen %d x %d' % (screen_width
, screen_height
))
173 # reset window position if more than 50% is off-screen
174 half_width
= cfg
.width
/ 2
175 half_height
= cfg
.height
/ 2
176 if (cfg
.x
+ cfg
.width
- half_width
) < 0 or (cfg
.y
+ cfg
.height
- half_height
) < 0 \
177 or cfg
.x
> (screen_width
- half_width
) or cfg
.y
> (screen_height
- half_height
):
178 logger
.warning('"%s" window was off-screen at (%d, %d), resetting to default position' % (config_prefix
, cfg
.x
, cfg
.y
))
182 if cfg
.width
!= -1 and cfg
.height
!= -1:
183 window
.resize(cfg
.width
, cfg
.height
)
185 # Not all platforms can natively restore position, gPodder must handle it.
186 # https://github.com/gpodder/gpodder/pull/933#issuecomment-818039693
187 if cfg
.x
== -1 or cfg
.y
== -1:
188 window
.set_position(Gtk
.WindowPosition
.CENTER_ON_PARENT
)
190 window
.move(cfg
.x
, cfg
.y
)
191 # From Gtk docs: most window managers ignore requests for initial window
192 # positions (instead using a user-defined placement algorithm) and honor
193 # requests after the window has already been shown.
194 # Move it a second time after the window has been shown.
195 # The first move reduces chance of window jumping,
196 # and gives the window manager a position to unmaximize to.
197 if not cfg
.maximized
:
198 util
.idle_add(window
.move
, cfg
.x
, cfg
.y
)
200 # Ignore events while we're connecting to the window
201 self
.__ignore
_window
_events
= True
203 # Get window state, correct size comes from window.get_size(),
204 # see https://developer.gnome.org/SaveWindowState/
205 def _receive_configure_event(widget
, event
):
206 if not self
.__ignore
_window
_events
:
207 # TODO: The maximize event might arrive after the configure event.
208 # This causes the maximized size to be saved, and restoring the
209 # window will not save its smaller size. Delaying the save with
210 # idle_add() is not enough time for the state event to arrive.
211 if not bool(event
.window
.get_state() & Gdk
.WindowState
.MAXIMIZED
):
212 x_pos
, y_pos
= widget
.get_position()
213 width_size
, height_size
= widget
.get_size()
216 cfg
.width
= width_size
217 cfg
.height
= height_size
219 window
.connect('configure-event', _receive_configure_event
)
221 def _receive_window_state(widget
, event
):
222 new_value
= bool(event
.new_window_state
& Gdk
.WindowState
.MAXIMIZED
)
223 cfg
.maximized
= new_value
225 window
.connect('window-state-event', _receive_window_state
)
227 # After the window has been set up, we enable events again
228 def _enable_window_events():
229 self
.__ignore
_window
_events
= False
230 util
.idle_add(_enable_window_events
)