From 3935f6ae5fec9ca72a4fbad83aaa0f79cfcf5d37 Mon Sep 17 00:00:00 2001 From: coderkun Date: Thu, 4 Dec 2014 21:14:23 +0100 Subject: [PATCH] redesign connection panel and use Avahi instead of profiles --- data/de.coderkun.MCG.gschema.xml | 25 +++- data/gschemas.compiled | Bin 668 -> 812 bytes gui/gtk.py | 240 +++++++++++++++---------------- mcg.py | 115 +-------------- 4 files changed, 143 insertions(+), 237 deletions(-) diff --git a/data/de.coderkun.MCG.gschema.xml b/data/de.coderkun.MCG.gschema.xml index 6ed2bc8..c532da1 100644 --- a/data/de.coderkun.MCG.gschema.xml +++ b/data/de.coderkun.MCG.gschema.xml @@ -1,6 +1,26 @@ + + 'localhost' + MPD host + MPD host to connect to + + + 6600 + MPD port + MPD port to connect to + + + '' + Image dir + Directory which a webserver is providing images on + + + false + Connection state + State of last connection + [800, 600] Window size @@ -11,11 +31,6 @@ Window maximized Window maximized state. - - 0 - Last selected profile - The index of the last selected profile used to connect to the MPD server. - 1 diff --git a/data/gschemas.compiled b/data/gschemas.compiled index 799f95f9834cc21c6dbbe56440673a45e2c4bd0b..ec3c06730c5d165c7804c268dd1b309b298427b5 100644 GIT binary patch literal 812 zcmZ<{ODxJv%qwAl0tp5Nh6o5-g9$=2FfuSO%(=VeB!~vd@iTZZWH2x=9AIQ%U=?Lx z`2YX^e+_1kVjl(&t;5W~kdmsGoS%|flwF#qmz$gplGNY85lO* z7X1zq2eAbh$`}}!7#LWY7#KvpHWY)zL2QsY0!$1HqD%}72WsoMLGA~!SsBV06qpzo zOqduL_MA>r2dM|KLFyft7#N~KW3xLF)=Xw;BxE&iG$c6^G`4_Fq~y#U?@vc2b%+8b1{@L z++bp05MyRw@JgHmHV4E8nWMnW01oTo{Gt-wlFEWqc-U$rX_jZ^rR10E7H3wag49KT z0$U?76DFRUSdp2VS(Tas)}xW6nVFlIo~oOYSp*W$DAv>~NGt(GFaraFeoCr7EF$%D zlhgGX7>YAXQggwEGcYhrV_;waX#<({0u-4VnHrTEnVFiJnhc=01jQ>zn3aKnft`VY zfg2jPAe%tW0;Q>f#JtoTkN`AofXoAlgY+>V%*)6xE@8;YPfpAMvlujrHIwu6@=}va zz&_DP(gX)aeo;zl5kqBaBFGqqWT?6S6N^eRi^0kKKS=0*NoGk-Dg%TKQT(5kL8Dls aGO<)svmn0+tdxP_gd_t4gGQz%NDlx%ah6X2 delta 464 zcmZ3(HivbBO??g{0|Tol1H=FS|Nl>AVqjok@L`z2#K16ziGd*{RWCU|CABEKG*2%# zIUOXa!3iQ67#KiGnHd-uV3>u0fq@l@*%=rZ#Fknd2GJn%*%`_hDi|3U8Wl0X_jY;J}!h69WY z3}+b`7`C2L02>5ivoe%1++bv2_{_+_P?n?)76-Ap7|OsdwPRvn*mF8f9b^uO&B;*4 z;K9Vekj&(U@O?7KOAI~?DNGCu4NMFSn{JDK2WbGY1sKX0CNMEDfI_ICC_gPTClwkF z3=9k!nVORiFp5qVWs;XG&&*57FV`*3tV(52U}j*502!*0n5kKin3tL}xr<4Wr#Qc; zL^r=ECAElwVe)k*d(O;~)LgKR$@WaL7HkX*44}XSnGXt3Mg|534ro9_P0vlN$jr^G RN=;#4&`8n*DP&+^002LBTD$-N diff --git a/gui/gtk.py b/gui/gtk.py index 2cb14db..777bb5b 100755 --- a/gui/gtk.py +++ b/gui/gtk.py @@ -18,6 +18,7 @@ import threading import urllib from gi.repository import Gio, Gtk, Gdk, GObject, GdkPixbuf, GLib +from gi.repository import Avahi import mcg @@ -27,9 +28,12 @@ import mcg class Application(Gtk.Application): TITLE = "MPDCoverGrid (Gtk)" SETTINGS_BASE_KEY = 'de.coderkun.mcg' + SETTING_HOST = 'host' + SETTING_PORT = 'port' + SETTING_CONNECTED = 'connected' + SETTING_IMAGE_DIR = 'image-dir' SETTING_WINDOW_SIZE = 'window-size' SETTING_WINDOW_MAXIMIZED = 'window-maximized' - SETTING_PROFILE = 'profile' SETTING_PANEL = 'panel' SETTING_ITEM_SIZE = 'item-size' SETTING_SORT_ORDER = 'sort-order' @@ -156,6 +160,9 @@ class Window(Gtk.ApplicationWindow): """) self.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), styleProvider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) self.get_style_context().add_class(Window.STYLE_CLASS_BG_TEXTURE) + self._panels[Window._PANEL_INDEX_CONNECTION].set_host(self._settings.get_string(Application.SETTING_HOST)) + self._panels[Window._PANEL_INDEX_CONNECTION].set_port(self._settings.get_int(Application.SETTING_PORT)) + self._panels[Window._PANEL_INDEX_CONNECTION].set_image_dir(self._settings.get_string(Application.SETTING_IMAGE_DIR)) self._panels[Window._PANEL_INDEX_PLAYLIST].set_item_size(self._settings.get_int(Application.SETTING_ITEM_SIZE)) self._panels[Window._PANEL_INDEX_LIBRARY].set_item_size(self._settings.get_int(Application.SETTING_ITEM_SIZE)) self._panels[Window._PANEL_INDEX_LIBRARY].set_sort_order(self._settings.get_string(Application.SETTING_SORT_ORDER)) @@ -169,7 +176,7 @@ class Window(Gtk.ApplicationWindow): self._header_bar.connect_signal(HeaderBar.SIGNAL_CONNECT, self.on_header_bar_connect) self._header_bar.connect_signal(HeaderBar.SIGNAL_PLAYPAUSE, self.on_header_bar_playpause) self._header_bar.connect_signal(HeaderBar.SIGNAL_SET_VOLUME, self.on_header_bar_set_volume) - self._panels[Window._PANEL_INDEX_CONNECTION].connect_signal(ConnectionPanel.SIGNAL_PROFILE_CHANGED, self.on_connection_panel_profile_changed) + self._panels[Window._PANEL_INDEX_CONNECTION].connect_signal(ConnectionPanel.SIGNAL_CONNECTION_CHANGED, self.on_connection_panel_connection_changed) self._panels[Window._PANEL_INDEX_PLAYLIST].connect_signal(PlaylistPanel.SIGNAL_CLEAR_PLAYLIST, self.on_playlist_panel_clear_playlist) self._panels[Window._PANEL_INDEX_COVER].connect_signal(CoverPanel.SIGNAL_TOGGLE_FULLSCREEN, self.on_cover_panel_toggle_fullscreen) self._panels[Window._PANEL_INDEX_COVER].connect_signal(CoverPanel.SIGNAL_SET_SONG, self.on_cover_panel_set_song) @@ -195,7 +202,8 @@ class Window(Gtk.ApplicationWindow): self.show_all() self._infobar.hide() self._stack.set_visible_child(self._panels[Window._PANEL_INDEX_CONNECTION]) - self._panels[Window._PANEL_INDEX_CONNECTION].select_profile(self._settings.get_int(Application.SETTING_PROFILE)) + if self._settings.get_boolean(Application.SETTING_CONNECTED): + self._connect() def on_resize(self, widget, event): @@ -234,10 +242,10 @@ class Window(Gtk.ApplicationWindow): # Panel callbacks - def on_connection_panel_profile_changed(self, index, profile): - self._settings.set_int(Application.SETTING_PROFILE, index) - if ConnectionPanel.TAG_AUTOCONNECT in profile.get_tags(): - self._connect() + def on_connection_panel_connection_changed(self, host, port, password, image_dir): + self._settings.set_string(Application.SETTING_HOST, host) + self._settings.set_int(Application.SETTING_PORT, port) + self._settings.set_string(Application.SETTING_IMAGE_DIR, image_dir) def on_playlist_panel_clear_playlist(self): @@ -353,12 +361,14 @@ class Window(Gtk.ApplicationWindow): self._header_bar.set_sensitive(False, True) if self._mcg.is_connected(): self._mcg.disconnect() + self._settings.set_boolean(Application.SETTING_CONNECTED, False) else: host = connection_panel.get_host() port = connection_panel.get_port() password = connection_panel.get_password() image_dir = connection_panel.get_image_dir() self._mcg.connect(host, port, password, image_dir) + self._settings.set_boolean(Application.SETTING_CONNECTED, True) def _connect_connected(self): @@ -564,90 +574,83 @@ class Panel(mcg.Base): -class ConnectionPanel(Panel, Gtk.HBox): - SIGNAL_PROFILE_CHANGED = 'profile-changed' - TAG_AUTOCONNECT = 'autoconnect' +class ConnectionPanel(Panel, Gtk.VBox): + SIGNAL_CONNECTION_CHANGED = 'connection-changed' def __init__(self): Panel.__init__(self) - Gtk.HBox.__init__(self) - self._profile_config = mcg.MCGProfileConfig() - self._profiles = Gtk.ListStore(str) + Gtk.VBox.__init__(self) + self._services = Gtk.ListStore(str, str, int) self._profile = None # Widgets - vbox = Gtk.VBox() - self.pack_start(vbox, True, False, 0) - self._table = Gtk.Table(6, 2, False) - vbox.pack_start(self._table, True, False, 0) - # Profile - profile_box = Gtk.HBox() - self._table.attach(profile_box, 0, 2, 0, 1) - # Profile Selection - self._profile_combo = Gtk.ComboBox.new_with_model(self._profiles) - self._profile_combo.set_entry_text_column(0) + hbox = Gtk.HBox() + self.pack_start(hbox, True, False, 0) + grid = Gtk.Grid() + grid.set_column_spacing(5) + grid.set_column_homogeneous(True) + hbox.pack_start(grid, True, False, 0) + # Zeroconf + zeroconf_box = Gtk.HBox() + grid.add(zeroconf_box) + # Zeroconf list + self._zeroconf_list = Gtk.TreeView(self._services) + self._zeroconf_list.get_selection().set_mode(Gtk.SelectionMode.SINGLE) renderer = Gtk.CellRendererText() - self._profile_combo.pack_start(renderer, True) - self._profile_combo.add_attribute(renderer, "text", 0) - profile_box.pack_start(self._profile_combo, True, True, 0) - # Profile Management - profile_button_box = Gtk.HBox() - profile_box.pack_end(profile_button_box, False, True, 0) - # New Profile - self._profile_new_button = Gtk.Button() - self._profile_new_button.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.BUTTON)) - profile_button_box.add(self._profile_new_button) - # Delete Profile - self._profile_delete_button = Gtk.Button() - self._profile_delete_button.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_DELETE, Gtk.IconSize.BUTTON)) - profile_button_box.add(self._profile_delete_button) + column = Gtk.TreeViewColumn("Zeroconf", renderer, text=0) + self._zeroconf_list.append_column(column) + zeroconf_box.pack_start(self._zeroconf_list, True, True, 0) + # Separator + separator = Gtk.Separator.new(Gtk.Orientation.VERTICAL) + zeroconf_box.pack_end(separator, False, False, 5) + # Connection grid + connection_grid = Gtk.Grid() + grid.attach_next_to(connection_grid, zeroconf_box, Gtk.PositionType.RIGHT, 1, 1) # Host host_label = Gtk.Label("Host:") host_label.set_alignment(0, 0.5) - self._table.attach(host_label, 0, 1, 1, 2) + connection_grid.add(host_label) self._host_entry = Gtk.Entry() self._host_entry.set_text("localhost") - self._table.attach(self._host_entry, 1, 2, 1, 2) + connection_grid.attach_next_to(self._host_entry, host_label, Gtk.PositionType.BOTTOM, 1, 1) # Port port_label = Gtk.Label("Port:") port_label.set_alignment(0, 0.5) - self._table.attach(port_label, 0, 1, 2, 3) + connection_grid.attach_next_to(port_label, self._host_entry, Gtk.PositionType.BOTTOM, 1, 1) adjustment = Gtk.Adjustment(6600, 1024, 9999, 1, 10, 10) self._port_spinner = Gtk.SpinButton() self._port_spinner.set_adjustment(adjustment) - self._table.attach(self._port_spinner, 1, 2, 2, 3) + connection_grid.attach_next_to(self._port_spinner, port_label, Gtk.PositionType.BOTTOM, 1, 1) # Passwort password_label = Gtk.Label("Password:") password_label.set_alignment(0, 0.5) - self._table.attach(password_label, 0, 1, 3, 4) + connection_grid.attach_next_to(password_label, self._port_spinner, Gtk.PositionType.BOTTOM, 1, 1) self._password_entry = Gtk.Entry() - self._table.attach(self._password_entry, 1, 2, 3, 4) + self._password_entry.set_input_purpose(Gtk.InputPurpose.PASSWORD) + self._password_entry.set_visibility(False) + connection_grid.attach_next_to(self._password_entry, password_label, Gtk.PositionType.BOTTOM, 1, 1) # Image dir image_dir_label = Gtk.Label("Image Dir:") image_dir_label.set_alignment(0, 0.5) - self._table.attach(image_dir_label, 0, 1, 4, 5) + connection_grid.attach_next_to(image_dir_label, self._password_entry, Gtk.PositionType.BOTTOM, 1, 1) self._image_dir_entry = Gtk.Entry() - self._table.attach(self._image_dir_entry, 1, 2, 4, 5) - # Autoconnect - self._autoconnect_button = Gtk.CheckButton("Autoconnect") - self._table.attach(self._autoconnect_button, 1, 2, 5, 6) + connection_grid.attach_next_to(self._image_dir_entry, image_dir_label, Gtk.PositionType.BOTTOM, 1, 1) + + # Zeroconf provider + self._zeroconf_provider = ZeroconfProvider() + self._zeroconf_provider.connect_signal(ZeroconfProvider.SIGNAL_SERVICE_NEW, self.on_new_service) # Signals - self._profiles.connect('row-changed', self.on_profiles_changed) - self._profiles.connect('row-inserted', self.on_profiles_changed) - self._profiles.connect('row-deleted', self.on_profiles_changed) - self._profile_combo.connect("changed", self.on_profile_combo_changed) - self._profile_new_button.connect('clicked', self.on_profile_new_clicked) - self._profile_delete_button.connect('clicked', self.on_profile_delete_clicked) + self._zeroconf_list.get_selection().connect('changed', self.on_service_selected) + self._zeroconf_list.connect('focus-out-event', self.on_zeroconf_list_outfocused) self._host_entry.connect('focus-out-event', self.on_host_entry_outfocused) self._port_spinner.connect('value-changed', self.on_port_spinner_value_changed) self._password_entry.connect('focus-out-event', self.on_password_entry_outfocused) self._image_dir_entry.connect('focus-out-event', self.on_image_dir_entry_outfocused) - self._autoconnect_button.connect('toggled', self.on_autoconnect_button_toggled) # Actions - self._load_profiles() + #self._load_profiles() def get_name(self): @@ -658,63 +661,37 @@ class ConnectionPanel(Panel, Gtk.HBox): return "Server" - def on_profiles_changed(self, *data): - self._profile_config.save() + def on_new_service(self, service): + name, host, port = service + self._services.append([name, host, port]) - def on_profile_combo_changed(self, combo): - (index, profile) = self._get_selected_profile() - if profile is not None: - self._profile = profile - self.set_host(self._profile.get('host')) - self.set_port(int(self._profile.get('port'))) - self.set_password(self._profile.get('password')) - self.set_image_dir(self._profile.get('image_dir')) - self._autoconnect_button.set_active(ConnectionPanel.TAG_AUTOCONNECT in self._profile.get_tags()) - self._callback(ConnectionPanel.SIGNAL_PROFILE_CHANGED, index, profile) + def on_service_selected(self, selection): + model, treeiter = selection.get_selected() + if treeiter != None: + service = model[treeiter] + self.set_host(service[1]) + self.set_port(service[2]) - def on_profile_new_clicked(self, widget): - profile = mcg.MCGProfile() - self._profile_config.add_profile(profile) - self._reload_profiles() - self._profile_combo.set_active(len(self._profiles)-1) - - - def on_profile_delete_clicked(self, widget): - (index, profile) = self._get_selected_profile() - if profile is not None: - self._profile_config.delete_profile(profile) - self._reload_profiles() - self._profile_combo.set_active(0) + def on_zeroconf_list_outfocused(self, widget, event): + self._zeroconf_list.get_selection().unselect_all() def on_host_entry_outfocused(self, widget, event): - self._profile.set('host', widget.get_text()) - self._profiles.set(self._profile_combo.get_active_iter(), 0, widget.get_text()) + self._call_back() def on_port_spinner_value_changed(self, widget): - self._profile.set('port', self.get_port()) + self._call_back() def on_password_entry_outfocused(self, widget, event): - self._profile.set('password', widget.get_text()) + self._call_back() def on_image_dir_entry_outfocused(self, widget, event): - self._profile.set('image_dir', widget.get_text()) - - - def on_autoconnect_button_toggled(self, widget): - tags = self._profile.get_tags() - if widget.get_active(): - if ConnectionPanel.TAG_AUTOCONNECT not in tags: - tags.append(ConnectionPanel.TAG_AUTOCONNECT) - else: - if ConnectionPanel.TAG_AUTOCONNECT in tags: - tags.remove(ConnectionPanel.TAG_AUTOCONNECT) - self._profile.set_tags(tags) + self._call_back() def set_host(self, host): @@ -754,31 +731,8 @@ class ConnectionPanel(Panel, Gtk.HBox): return self._image_dir_entry.get_text() - def select_profile(self, index): - if len(self._profiles) <= index: - index = 0 - self._profile_combo.set_active(index) - - - def _load_profiles(self): - self._profile_config.load() - for profile in self._profile_config.get_profiles(): - self._profiles.append([str(profile)]) - - - def _reload_profiles(self): - self._profiles.clear() - for profile in self._profile_config.get_profiles(): - self._profiles.append([str(profile)]) - - - def _get_selected_profile(self): - index = self._profile_combo.get_active() - if index >= 0: - profiles = self._profile_config.get_profiles() - if index < len(profiles): - return (index, profiles[index]) - return (-1, None) + def _call_back(self): + self._callback(ConnectionPanel.SIGNAL_CONNECTION_CHANGED, self.get_host(), self.get_port(), self.get_password(), self.get_image_dir()) @@ -1493,3 +1447,45 @@ class StackSwitcher(mcg.Base, Gtk.StackSwitcher): else: self._temp_button = None self._callback(StackSwitcher.SIGNAL_STACK_SWITCHED, self) + + + + +class ZeroconfProvider(mcg.Base): + SIGNAL_SERVICE_NEW = 'service-new' + TYPE = '_mpd._tcp' + + + def __init__(self): + mcg.Base.__init__(self) + self._service_resolvers = [] + self._services = {} + # Client + self._client = Avahi.Client(flags=0,) + self._client.start() + # Browser + self._service_browser = Avahi.ServiceBrowser(domain='local', flags=0, interface=-1, protocol=Avahi.Protocol.GA_PROTOCOL_UNSPEC, type=ZeroconfProvider.TYPE) + self._service_browser.connect('new_service', self.on_new_service) + self._service_browser.attach(self._client) + + + def on_new_service(self, browser, interface, protocol, name, type, domain, flags): + if not (flags & Avahi.LookupResultFlags.GA_LOOKUP_RESULT_LOCAL): + service_resolver = Avahi.ServiceResolver(interface=interface, protocol=protocol, name=name, type=type, domain=domain, aprotocol=Avahi.Protocol.GA_PROTOCOL_UNSPEC, flags=0,) + service_resolver.connect('found', self.on_found) + service_resolver.connect('failure', self.on_failure) + service_resolver.attach(self._client) + self._service_resolvers.append(service_resolver) + + + def on_found(self, resolver, interface, protocol, name, type, domain, host, date, port, *args): + if (host, port) not in self._services.keys(): + service = (name,host,port) + self._services[(host,port)] = service + self._callback(ZeroconfProvider.SIGNAL_SERVICE_NEW, service) + + + def on_failure(self, resolver, date): + if resolver in self._service_resolvers: + self._service_resolvers.remove(resolver) + diff --git a/mcg.py b/mcg.py index 9b27c72..140fc23 100644 --- a/mcg.py +++ b/mcg.py @@ -202,6 +202,11 @@ class Client(Base): self._add_action(self._set_volume, volume) + def test(self): + self._logger.info("test") + self._add_action(self._test) + + # Private methods def _connect(self, host, port, password): @@ -836,116 +841,6 @@ class MCGConfig(configparser.ConfigParser): -class MCGProfileConfig(MCGConfig): - CONFIG_FILE = 'profiles.conf' - - - def __init__(self): - MCGConfig.__init__(self, MCGProfileConfig.CONFIG_FILE) - self._profiles = [] - - - def add_profile(self, profile): - self._profiles.append(profile) - - - def delete_profile(self, profile): - if profile in self._profiles: - self._profiles.remove(profile) - self._force_default_profile() - - - def get_profiles(self): - return self._profiles - - - def load(self): - super().load() - count = 0 - if self.has_section('profiles'): - if self.has_option('profiles', 'count'): - count = self.getint('profiles', 'count') - for index in range(count): - section = 'profile'+str(index+1) - if self.has_section(section): - profile = MCGProfile() - for attribute in profile.get_attributes(): - if self.has_option(section, attribute): - profile.set(attribute, self.get(section, attribute)) - self._profiles.append(profile) - self._force_default_profile() - - - def save(self): - if not self.has_section('profiles'): - self.add_section('profiles') - self.set('profiles', 'count', str(len(self._profiles))) - - for index in range(len(self._profiles)): - profile = self._profiles[index] - section = 'profile'+str(index+1) - if not self.has_section(section): - self.add_section(section) - for attribute in profile.get_attributes(): - self.set(section, attribute, str(profile.get(attribute))) - for section in self.sections()[len(self._profiles)+1:]: - self.remove_section(section) - super().save() - - - def _force_default_profile(self): - if len(self._profiles) == 0: - self._profiles.append(MCGProfile()) - - - - -class MCGConfigurable: - def __init__(self): - self._attributes = [] - - - def get(self, attribute): - return getattr(self, attribute) - - - def set(self, attribute, value): - setattr(self, attribute, value) - if attribute not in self._attributes: - self._attributes.append(attribute) - - - def get_attributes(self): - return self._attributes - - - - -class MCGProfile(MCGConfigurable): - - def __init__(self): - MCGConfigurable.__init__(self) - self.set('host', "localhost") - self.set('port', 6600) - self.set('password', "") - self.set('image_dir', "") - self.set('tags', "") - - - def __str__(self): - return self.get("host") - - - def get_tags(self): - return self.get('tags').split(',') - - - def set_tags(self, tags): - self.set('tags', ','.join(tags)) - - - - class MCGCache(): DIRNAME = '~/.cache/mcg/' SIZE_FILENAME = 'size'