Split out GTK+-related parts of gpodder.config module

Create a new UIConfig class that subclasses the normal
Config class that takes care of the basic configuration
work.

The UIConfig class adds support for registering GTK+ UI
elements with the configuration object.

Additionally, the ListStore (TreeModel) code is split
into a new class ConfigModel that takes care of formatting
the configuration data for display in GTK+ applications.
This commit is contained in:
Thomas Perl 2009-08-24 15:31:25 +02:00
parent c6fc281820
commit a6c54819d1
5 changed files with 252 additions and 164 deletions

View file

@ -15,6 +15,7 @@
<property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
<property name="focus_on_map">True</property>
<property name="urgency_hint">False</property>
<signal handler="on_gPodderConfigEditor_destroy" name="destroy"/>
<child internal-child="vbox">
<object class="GtkVBox" id="vbox13">
<property name="visible">True</property>

View file

@ -24,15 +24,12 @@
#
import gtk
import pango
import gpodder
from gpodder import util
from gpodder.liblogger import log
import atexit
import os.path
import os
import time
import threading
import ConfigParser
@ -238,21 +235,18 @@ class Config(dict):
# Number of seconds after which settings are auto-saved
WRITE_TO_DISK_TIMEOUT = 60
def __init__( self, filename = 'gpodder.conf'):
dict.__init__( self)
def __init__(self, filename='gpodder.conf'):
dict.__init__(self)
self.__save_thread = None
self.__filename = filename
self.__section = 'gpodder-conf-1'
self.__ignore_window_events = False
self.__observers = []
# Name, Type, Value, Type(python type), Editable?, Font style, Boolean?, Boolean value
self.__model = gtk.ListStore(str, str, str, object, bool, int, bool, bool)
atexit.register( self.__atexit)
self.load()
self.apply_fixes()
atexit.register( self.__atexit)
def apply_fixes(self):
# Here you can add fixes in case syntax changes. These will be
# applied whenever a configuration file is loaded.
@ -260,15 +254,14 @@ class Config(dict):
log('Fixing OLD syntax {channel.*} => {podcast.*} in custom_sync_name.', sender=self)
self.custom_sync_name = self.custom_sync_name.replace('{channel.', '{podcast.')
def __getattr__( self, name):
def __getattr__(self, name):
if name in self.Settings:
( fieldtype, default ) = self.Settings[name][:2]
return self[name]
else:
raise AttributeError('%s is not a setting' % name)
def get_description( self, option_name ):
description = _("No description available.")
def get_description(self, option_name):
description = _('No description available.')
if self.Settings.get(option_name) is not None:
row = self.Settings[option_name]
@ -293,161 +286,81 @@ class Config(dict):
else:
log('Observer already added: %s', repr(callback), sender=self)
def connect_gtk_editable( self, name, editable):
if name in self.Settings:
editable.delete_text( 0, -1)
editable.insert_text( str(getattr( self, name)))
editable.connect( 'changed', lambda editable: setattr( self, name, editable.get_chars( 0, -1)))
def remove_observer(self, callback):
"""
Remove an observer previously added to this object.
"""
if callback in self.__observers:
self.__observers.remove(callback)
else:
raise ValueError( '%s is not a setting' % name)
log('Observer not added :%s', repr(callback), sender=self)
def connect_gtk_spinbutton( self, name, spinbutton):
if name in self.Settings:
spinbutton.set_value( getattr( self, name))
spinbutton.connect( 'value-changed', lambda spinbutton: setattr( self, name, spinbutton.get_value()))
else:
raise ValueError( '%s is not a setting' % name)
def connect_gtk_paned( self, name, paned):
if name in self.Settings:
paned.set_position( getattr( self, name))
paned_child = paned.get_child1()
paned_child.connect( 'size-allocate', lambda x, y: setattr( self, name, paned.get_position()))
else:
raise ValueError( '%s is not a setting' % name)
def connect_gtk_togglebutton( self, name, togglebutton):
if name in self.Settings:
togglebutton.set_active( getattr( self, name))
togglebutton.connect( 'toggled', lambda togglebutton: setattr( self, name, togglebutton.get_active()))
else:
raise ValueError( '%s is not a setting' % name)
def filechooser_selection_changed(self, name, filechooser):
filename = filechooser.get_filename()
if filename is not None:
setattr(self, name, filename)
def connect_gtk_filechooser(self, name, filechooser, is_for_files=False):
if name in self.Settings:
if is_for_files:
# A FileChooser for a single file
filechooser.set_filename(getattr(self, name))
else:
# A FileChooser for a folder
filechooser.set_current_folder(getattr(self, name))
filechooser.connect('selection-changed', lambda filechooser: self.filechooser_selection_changed(name, filechooser))
else:
raise ValueError('%s is not a setting'%name)
def receive_configure_event( self, widget, event, config_prefix):
(x, y, width, height, maximized) = map(lambda x: config_prefix + '_' + x, ['x', 'y', 'width', 'height', 'maximized'])
( x_pos, y_pos ) = widget.get_position()
( width_size, height_size ) = widget.get_size()
if not self.__ignore_window_events and not (hasattr(self, maximized) and getattr(self, maximized)):
setattr( self, x, x_pos)
setattr( self, y, y_pos)
setattr( self, width, width_size)
setattr( self, height, height_size)
def receive_window_state(self, widget, event, config_prefix):
if hasattr(self, config_prefix+'_maximized'):
setattr(self, config_prefix+'_maximized', bool(event.new_window_state & gtk.gdk.WINDOW_STATE_MAXIMIZED))
def enable_window_events(self):
self.__ignore_window_events = False
def disable_window_events(self):
self.__ignore_window_events = True
def connect_gtk_window( self, window, config_prefix, show_window=False):
(x, y, width, height, maximized) = map(lambda x: config_prefix + '_' + x, ['x', 'y', 'width', 'height', 'maximized'])
if set( ( x, y, width, height )).issubset( set( self.Settings)):
window.resize( getattr( self, width), getattr( self, height))
window.move( getattr( self, x), getattr( self, y))
self.disable_window_events()
util.idle_add(self.enable_window_events)
window.connect('configure-event', self.receive_configure_event, config_prefix)
window.connect('window-state-event', self.receive_window_state, config_prefix)
if show_window:
window.show()
if hasattr(self, maximized) and getattr(self, maximized) == True:
window.maximize()
else:
raise ValueError( 'Missing settings in set: %s' % ', '.join( ( x, y, width, height )))
def schedule_save( self):
def schedule_save(self):
if self.__save_thread is None:
self.__save_thread = threading.Thread( target = self.save_thread_proc)
self.__save_thread = threading.Thread(target=self.save_thread_proc)
self.__save_thread.setDaemon(True)
self.__save_thread.start()
def save_thread_proc( self):
for i in range(self.WRITE_TO_DISK_TIMEOUT*10):
if self.__save_thread is not None:
time.sleep( .1)
def save_thread_proc(self):
time.sleep(self.WRITE_TO_DISK_TIMEOUT)
if self.__save_thread is not None:
self.save()
def __atexit( self):
def __atexit(self):
if self.__save_thread is not None:
self.save()
def save( self, filename = None):
if filename is not None:
self.__filename = filename
def save(self, filename=None):
if filename is None:
filename = self.__filename
log( 'Flushing settings to disk', sender = self)
log('Flushing settings to disk', sender=self)
parser = ConfigParser.RawConfigParser()
parser.add_section( self.__section)
parser.add_section(self.__section)
for ( key, value ) in self.Settings.items():
( fieldtype, default ) = value[:2]
parser.set( self.__section, key, getattr( self, key, default))
for key, value in self.Settings.items():
fieldtype, default = value[:2]
parser.set(self.__section, key, getattr(self, key, default))
try:
parser.write( open( self.__filename, 'w'))
parser.write(open(filename, 'w'))
except:
raise IOError( 'Cannot write to file: %s' % self.__filename)
log('Cannot write settings to %s', filename, sender=self)
raise IOError('Cannot write to file: %s' % filename)
self.__save_thread = None
def load( self, filename = None):
def load(self, filename=None):
if filename is not None:
self.__filename = filename
self.__model.clear()
parser = ConfigParser.RawConfigParser()
try:
parser.read( self.__filename)
except:
pass
for key in sorted(self.Settings):
(fieldtype, default) = self.Settings[key][:2]
if os.path.exists(self.__filename):
try:
parser.read(self.__filename)
except:
log('Cannot parse config file: %s', self.__filename,
sender=self, traceback=True)
for key, value in self.Settings.items():
fieldtype, default = value[:2]
try:
if fieldtype == int:
value = parser.getint( self.__section, key)
value = parser.getint(self.__section, key)
elif fieldtype == float:
value = parser.getfloat( self.__section, key)
value = parser.getfloat(self.__section, key)
elif fieldtype == bool:
value = parser.getboolean( self.__section, key)
value = parser.getboolean(self.__section, key)
else:
value = fieldtype(parser.get( self.__section, key))
value = fieldtype(parser.get(self.__section, key))
except:
log('Invalid value in %s for %s: %s', self.__filename,
key, value, sender=self, traceback=True)
value = default
self[key] = value
if value == default:
style = pango.STYLE_NORMAL
else:
style = pango.STYLE_ITALIC
self.__model.append([key, self.type_as_string(fieldtype), str(value), fieldtype, fieldtype is not bool, style, fieldtype is bool, bool(value)])
def model(self):
return self.__model
def toggle_flag(self, name):
if name in self.Settings:
@ -473,43 +386,25 @@ class Config(dict):
log('Invalid setting name: %s', name, sender=self)
return False
def type_as_string(self, type):
if type == int:
return _('Integer')
elif type == float:
return _('Float')
elif type == bool:
return _('Boolean')
else:
return _('String')
def __setattr__( self, name, value):
def __setattr__(self, name, value):
if name in self.Settings:
( fieldtype, default ) = self.Settings[name][:2]
fieldtype, default = self.Settings[name][:2]
try:
if self[name] != fieldtype(value):
log( 'Update: %s = %s', name, value, sender = self)
old_value = self[name]
log('Update %s: %s => %s', name, old_value, value, sender=self)
self[name] = fieldtype(value)
for observer in self.__observers:
try:
# Notify observer about config change
observer(name, old_value, self[name])
except:
log('Error while calling observer: %s', repr(observer), sender=self)
for row in self.__model:
if row[0] == name:
value = fieldtype(value)
row[2] = str(value)
row[7] = bool(value)
if self[name] == default:
style = pango.STYLE_NORMAL
else:
style = pango.STYLE_ITALIC
row[5] = style
log('Error while calling observer: %s',
repr(observer), sender=self,
traceback=True)
self.schedule_save()
except:
raise ValueError( '%s has to be of type %s' % ( name, fieldtype.__name__ ))
raise ValueError('%s has to be of type %s' % (name, fieldtype.__name__))
else:
object.__setattr__( self, name, value)
object.__setattr__(self, name, value)

188
src/gpodder/gtkui/config.py Normal file
View file

@ -0,0 +1,188 @@
# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client
# Copyright (c) 2005-2009 Thomas Perl and the gPodder Team
#
# gPodder is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# gPodder is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#
# gpodder.gtkui.config -- Config object with GTK+ support (2009-08-24)
#
import gtk
import pango
import gpodder
from gpodder import util
from gpodder import config
_ = gpodder.gettext
class ConfigModel(gtk.ListStore):
C_NAME, C_TYPE_TEXT, C_VALUE_TEXT, C_TYPE, C_EDITABLE, C_FONT_STYLE, \
C_IS_BOOLEAN, C_BOOLEAN_VALUE = range(8)
def __init__(self, config):
gtk.ListStore.__init__(self, str, str, str, object, \
bool, int, bool, bool)
self._config = config
self._fill_model()
self._config.add_observer(self._on_update)
def _type_as_string(self, type):
if type == int:
return _('Integer')
elif type == float:
return _('Float')
elif type == bool:
return _('Boolean')
else:
return _('String')
def _fill_model(self):
self.clear()
for key in sorted(self._config.Settings):
fieldtype, default = self._config.Settings[key][:2]
value = getattr(self._config, key, default)
if value == default:
style = pango.STYLE_NORMAL
else:
style = pango.STYLE_ITALIC
self.append((key, self._type_as_string(fieldtype), \
str(value), fieldtype, fieldtype is not bool, style, \
fieldtype is bool, bool(value)))
def _on_update(self, name, old_value, new_value):
for row in self:
if row[self.C_NAME] == name:
if new_value == self._config.Settings[name][1]:
style = pango.STYLE_NORMAL
else:
style = pango.STYLE_ITALIC
self.set(row.iter, \
self.C_VALUE_TEXT, str(new_value), \
self.C_BOOLEAN_VALUE, bool(new_value), \
self.C_FONT_STYLE, style)
break
def stop_observing(self):
self._config.remove_observer(self._on_update)
class UIConfig(config.Config):
def __init__(self, filename='gpodder.conf'):
config.Config.__init__(self, filename)
self.__ignore_window_events = False
def connect_gtk_editable(self, name, editable):
assert name in self.Settings
editable.delete_text(0, -1)
editable.insert_text(str(getattr(self, name)))
def _editable_changed(editable):
setattr(self, name, editable.get_chars(0, -1))
editable.connect('changed', _editable_changed)
def connect_gtk_spinbutton(self, name, spinbutton):
assert name in self.Settings
spinbutton.set_value(getattr(self, name))
def _spinbutton_changed(spinbutton):
setattr(self, name, spinbutton.get_value())
spinbutton.connect('value-changed', _spinbutton_changed)
def connect_gtk_paned(self, name, paned):
assert name in self.Settings
paned.set_position(getattr(self, name))
paned_child = paned.get_child1()
def _child_size_allocate(x, y):
setattr(self, name, paned.get_position())
paned_child.connect('size-allocate', _child_size_allocate)
def connect_gtk_togglebutton(self, name, togglebutton):
assert name in self.Settings
togglebutton.set_active(getattr(self, name))
def _togglebutton_toggled(togglebutton):
setattr(self, name, togglebutton.get_active())
togglebutton.connect('toggled', _togglebutton_toggled)
def connect_gtk_filechooser(self, name, filechooser, is_for_files=False):
assert name in self.Settings
# FIXME: can we determine "is_for_files" by consulting the filechooser?
if is_for_files:
# A FileChooser for a single file
filechooser.set_filename(getattr(self, name))
else:
# A FileChooser for a folder
filechooser.set_current_folder(getattr(self, name))
def _chooser_selection_changed(filechooser):
filename = filechooser.get_filename()
if filename is not None:
setattr(self, name, filename)
filechooser.connect('selection-changed', _chooser_selection_changed)
def connect_gtk_window(self, window, config_prefix, show_window=False):
x, y, width, height, maximized = map(lambda x: config_prefix+'_'+x, \
('x', 'y', 'width', 'height', 'maximized'))
if set((x, y, width, height)).issubset(set(self.Settings)):
window.resize(getattr(self, width), getattr(self, height))
window.move(getattr(self, x), getattr(self, y))
# Ignore events while we're connecting to the window
self.__ignore_window_events = True
def _receive_configure_event(widget, event):
x_pos, y_pos = widget.get_position()
width_size, height_size = widget.get_size()
if not self.__ignore_window_events and not \
(hasattr(self, maximized) and getattr(self, maximized)):
setattr(self, x, x_pos)
setattr(self, y, y_pos)
setattr(self, width, width_size)
setattr(self, height, height_size)
window.connect('configure-event', _receive_configure_event)
def _receive_window_state(widget, event):
new_value = bool(event.new_window_state & \
gtk.gdk.WINDOW_STATE_MAXIMIZED)
if hasattr(self, maximized):
setattr(self, maximized, new_value)
window.connect('window-state-event', _receive_window_state)
# After the window has been set up, we enable events again
def _enable_window_events():
self.__ignore_window_events = False
util.idle_add(_enable_window_events)
if show_window:
window.show()
if getattr(self, maximized, False):
window.maximize()
else:
raise ValueError('Cannot connect %s', config_prefix, sender=self)

View file

@ -95,6 +95,7 @@ from gpodder.gtkui.base import GtkBuilderWidget
from gpodder.gtkui.model import PodcastListModel
from gpodder.gtkui.model import EpisodeListModel
from gpodder.gtkui.opml import OpmlListModel
from gpodder.gtkui.config import ConfigModel
from gpodder.libgpodder import db
from gpodder.libgpodder import gl
@ -4590,7 +4591,7 @@ class gPodderConfigEditor(BuilderWidget):
value_renderer.connect('edited', self.value_edited)
self.configeditor.append_column(value_column)
self.model = gl.config.model()
self.model = ConfigModel(gl.config)
self.filter = self.model.filter_new()
self.filter.set_visible_func(self.visible_func)
@ -4637,6 +4638,9 @@ class gPodderConfigEditor(BuilderWidget):
def on_btnClose_clicked(self, widget):
self.gPodderConfigEditor.destroy()
def on_gPodderConfigEditor_destroy(self, widget):
self.model.stop_observing()
def on_configeditor_row_changed(self, treeselection):
model, iter = treeselection.get_selected()
if iter is not None:

View file

@ -28,7 +28,7 @@ from collections import defaultdict
import gpodder
from gpodder import util
from gpodder import config
from gpodder.gtkui import config
from gpodder import dbsqlite
import os
@ -43,7 +43,7 @@ _ = gpodder.gettext
class gPodderLib(object):
def __init__( self):
util.make_directory(gpodder.home)
self.config = config.Config(gpodder.config_file)
self.config = config.UIConfig(gpodder.config_file)
if gpodder.interface == gpodder.MAEMO:
# Detect changing of SD cards between mmc1/mmc2 if a gpodder