From 6e347b6b362c7526c3cc8fd8cf30bdacfdb1f7d6 Mon Sep 17 00:00:00 2001 From: Mon Zafra Date: Wed, 4 Jan 2017 16:37:13 +0800 Subject: [PATCH] Materialized dialogs and preferences (#330) * Changed platform AlertDialogs and preference.* to support lib counterparts - added dependencies: support/preference-v7 for PreferenceFragmentCompat and Preference subclasses, support/preference-v14 for the MultiSelectListPreference - simplified some AlertDialog.Builder calls and added non-null annotations to DialogFragment#onCreateDialog(Bundle) overrides to shut the IDE up - UIUtils: changed static member avatarColorsIdx to local var because it's only used in one place and the value isn't cached - layout/dialog_send_text: removed view vertical margins as they take way too much space for nothing. - strings: shortened english preference titles - themes: added PrefTheme and changed preference title font size to medium from large - preferences: changed CheckBoxPreference to SwitchPreferenceCompat. these don't have the same issue described in #233 (tested in kitkat). * Changed platform PreferenceManager in RemoteActivity to support pref * Fixed M permissions * Split prefs into 2 groups as per material design guidelines * Changed prefs theme to v14.material * Moved container padding to individual prefs; removed pref-v7 dependency - this makes the item dividers touch the screen edges which i think looks better - don't need to require preference-v7 because preference-v14 already does * Moved PrefTheme attributes *{Start,End} to v17 override * Fixed crash caused by rotating twice while a dialog is active * Changed wording as suggested --- app/build.gradle | 1 + .../kore/ui/generic/GenericSelectDialog.java | 4 +- .../ui/generic/SendTextDialogFragment.java | 4 +- .../audio/MusicVideoDetailsFragment.java | 2 +- .../ui/sections/hosts/HostListFragment.java | 14 +-- .../ui/sections/remote/RemoteActivity.java | 2 +- .../settings/AboutDialogFragment.java | 26 ++--- .../sections/settings/SettingsActivity.java | 18 +-- .../sections/settings/SettingsFragment.java | 32 +++--- .../sections/video/MovieDetailsFragment.java | 2 +- .../video/TVShowEpisodeDetailsFragment.java | 2 +- .../java/org/xbmc/kore/utils/UIUtils.java | 11 +- app/src/main/res/layout/dialog_send_text.xml | 6 +- app/src/main/res/values-v17/themes.xml | 22 ++++ app/src/main/res/values/strings.xml | 16 +-- app/src/main/res/values/themes.xml | 9 ++ app/src/main/res/xml/preferences.xml | 106 +++++++++--------- 17 files changed, 153 insertions(+), 124 deletions(-) create mode 100644 app/src/main/res/values-v17/themes.xml diff --git a/app/build.gradle b/app/build.gradle index fcb576b..4438b39 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -109,6 +109,7 @@ dependencies { compile 'com.android.support:support-v4:25.1.0' compile 'com.android.support:appcompat-v7:25.1.0' compile 'com.android.support:cardview-v7:25.1.0' + compile 'com.android.support:preference-v14:25.1.0' compile 'com.android.support:support-v13:25.1.0' compile 'com.fasterxml.jackson.core:jackson-databind:2.5.2' diff --git a/app/src/main/java/org/xbmc/kore/ui/generic/GenericSelectDialog.java b/app/src/main/java/org/xbmc/kore/ui/generic/GenericSelectDialog.java index 8dae043..1043d6b 100644 --- a/app/src/main/java/org/xbmc/kore/ui/generic/GenericSelectDialog.java +++ b/app/src/main/java/org/xbmc/kore/ui/generic/GenericSelectDialog.java @@ -16,11 +16,12 @@ package org.xbmc.kore.ui.generic; import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; import android.content.DialogInterface; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; /** * Dialog fragment that presents a list options to the user. @@ -121,6 +122,7 @@ public class GenericSelectDialog * * @return Dialog to select calendars */ + @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/app/src/main/java/org/xbmc/kore/ui/generic/SendTextDialogFragment.java b/app/src/main/java/org/xbmc/kore/ui/generic/SendTextDialogFragment.java index d5ba51c..c3d40e6 100644 --- a/app/src/main/java/org/xbmc/kore/ui/generic/SendTextDialogFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/generic/SendTextDialogFragment.java @@ -16,11 +16,12 @@ package org.xbmc.kore.ui.generic; import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; import android.content.DialogInterface; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; import android.view.KeyEvent; import android.view.View; import android.view.WindowManager; @@ -94,6 +95,7 @@ public class SendTextDialogFragment extends DialogFragment { * @param savedInstanceState Saved state * @return Created dialog */ + @NonNull @Override @SuppressWarnings("InflateParams") public Dialog onCreateDialog(Bundle savedInstanceState) { diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/audio/MusicVideoDetailsFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/audio/MusicVideoDetailsFragment.java index 90b64d4..4dba943 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/audio/MusicVideoDetailsFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/audio/MusicVideoDetailsFragment.java @@ -16,7 +16,6 @@ package org.xbmc.kore.ui.sections.audio; import android.annotation.TargetApi; -import android.app.AlertDialog; import android.content.DialogInterface; import android.content.res.Resources; import android.content.res.TypedArray; @@ -30,6 +29,7 @@ import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.app.AlertDialog; import android.text.TextUtils; import android.util.DisplayMetrics; import android.view.LayoutInflater; diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/hosts/HostListFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/hosts/HostListFragment.java index 171ed5b..16db83d 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/hosts/HostListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/hosts/HostListFragment.java @@ -15,15 +15,16 @@ */ package org.xbmc.kore.ui.sections.hosts; -import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.os.Handler; +import android.support.annotation.NonNull; import android.support.v4.app.DialogFragment; import android.support.v4.app.Fragment; +import android.support.v7.app.AlertDialog; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -371,11 +372,11 @@ public class HostListFragment extends Fragment { return frag; } + @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - // Use the Builder class for convenient dialog construction - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(R.string.delete_xbmc) + return new AlertDialog.Builder(getActivity()) + .setTitle(R.string.delete_xbmc) .setMessage(R.string.delete_xbmc_confirm) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { @@ -386,9 +387,8 @@ public class HostListFragment extends Fragment { public void onClick(DialogInterface dialog, int id) { mListener.onDialogNegativeClick(); } - }); - // Create the AlertDialog object and return it - return builder.create(); + }) + .create(); } } } diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteActivity.java b/app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteActivity.java index f92428d..93ac523 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteActivity.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteActivity.java @@ -20,11 +20,11 @@ import android.graphics.Point; import android.net.Uri; import android.os.Bundle; import android.os.Handler; -import android.preference.PreferenceManager; import android.support.v4.text.TextDirectionHeuristicsCompat; import android.support.v4.view.ViewPager; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBar; +import android.support.v7.preference.PreferenceManager; import android.support.v7.widget.Toolbar; import android.text.TextUtils; import android.view.KeyEvent; diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/settings/AboutDialogFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/settings/AboutDialogFragment.java index 7c26094..d06e137 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/settings/AboutDialogFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/settings/AboutDialogFragment.java @@ -5,12 +5,12 @@ package org.xbmc.kore.ui.sections.settings; import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.DialogFragment; -import android.content.DialogInterface; import android.content.pm.PackageManager; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; import android.text.Html; import android.text.method.LinkMovementMethod; import android.view.View; @@ -25,11 +25,10 @@ import org.xbmc.kore.R; public class AboutDialogFragment extends DialogFragment { + @NonNull @Override @SuppressWarnings("InflateParams") public Dialog onCreateDialog(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - Activity activity = getActivity(); View mainView = activity.getLayoutInflater().inflate(R.layout.fragment_about, null); @@ -46,18 +45,9 @@ public class AboutDialogFragment about.setText(Html.fromHtml(getString(R.string.about_desc))); about.setMovementMethod(LinkMovementMethod.getInstance()); - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - - // Build the dialog - builder.setView(mainView) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dismiss(); - } - }); - - return builder.create(); - + return new AlertDialog.Builder(activity) + .setView(mainView) + .setPositiveButton(android.R.string.ok, null) + .create(); } } diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/settings/SettingsActivity.java b/app/src/main/java/org/xbmc/kore/ui/sections/settings/SettingsActivity.java index c1a48ac..77c75c9 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/settings/SettingsActivity.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/settings/SettingsActivity.java @@ -18,8 +18,9 @@ package org.xbmc.kore.ui.sections.settings; import android.content.SharedPreferences; import android.os.Bundle; import android.preference.PreferenceManager; +import android.support.v4.app.FragmentManager; import android.support.v7.app.ActionBar; -import android.support.v7.app.ActionBarActivity; +import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.view.MenuItem; @@ -31,11 +32,9 @@ import org.xbmc.kore.utils.UIUtils; /** * Presents the Preferences fragment */ -public class SettingsActivity extends ActionBarActivity{ +public class SettingsActivity extends AppCompatActivity { private static final String TAG = LogUtils.makeLogTag(SettingsActivity.class); - private SettingsFragment settingsFragment; - @Override protected void onCreate(Bundle savedInstanceState) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); @@ -50,14 +49,17 @@ public class SettingsActivity extends ActionBarActivity{ Toolbar toolbar = (Toolbar)findViewById(R.id.default_toolbar); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); + assert actionBar != null; actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(R.string.settings); // Display the fragment as the main content. - settingsFragment = new SettingsFragment(); - getFragmentManager().beginTransaction() - .replace(R.id.fragment_container, settingsFragment) - .commit(); + FragmentManager fm = getSupportFragmentManager(); + if (fm.findFragmentByTag("settings-fragment") == null) { + fm.beginTransaction() + .replace(R.id.fragment_container, new SettingsFragment(), "settings-fragment") + .commit(); + } } @Override diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/settings/SettingsFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/settings/SettingsFragment.java index d9de139..1be643c 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/settings/SettingsFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/settings/SettingsFragment.java @@ -20,15 +20,13 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.Bundle; -import android.preference.ListPreference; -import android.preference.MultiSelectListPreference; -import android.preference.Preference; -import android.preference.PreferenceFragment; -import android.preference.TwoStatePreference; import android.support.annotation.NonNull; -import android.support.v13.app.FragmentCompat; import android.support.v4.app.TaskStackBuilder; import android.support.v4.content.ContextCompat; +import android.support.v7.preference.ListPreference; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceFragmentCompat; +import android.support.v7.preference.TwoStatePreference; import android.widget.Toast; import org.xbmc.kore.R; @@ -45,7 +43,7 @@ import java.lang.reflect.Method; /** * Simple fragment to display preferences screen */ -public class SettingsFragment extends PreferenceFragment +public class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = LogUtils.makeLogTag(SettingsFragment.class); @@ -53,17 +51,16 @@ public class SettingsFragment extends PreferenceFragment private int hostId; @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { // Load the preferences from an XML resource addPreferencesFromResource(R.xml.preferences); // Get the preference for side menu itens and change its Id to include // the current host - MultiSelectListPreference sideMenuItens = (MultiSelectListPreference)findPreference(Settings.KEY_PREF_NAV_DRAWER_ITEMS); + Preference sideMenuItems = findPreference(Settings.KEY_PREF_NAV_DRAWER_ITEMS); hostId = HostManager.getInstance(getActivity()).getHostInfo().getId(); - sideMenuItens.setKey(Settings.getNavDrawerItemsPrefKey(hostId)); + sideMenuItems.setKey(Settings.getNavDrawerItemsPrefKey(hostId)); // HACK: After changing the key dinamically like above, we need to force the preference // to read its value. This can be done by calling onSetInitialValue, which is protected, @@ -72,12 +69,12 @@ public class SettingsFragment extends PreferenceFragment // Furthermore, only do this is nothing is saved yet on the shared preferences, // otherwise the defaults won't be applied if (getPreferenceManager().getSharedPreferences().getStringSet(Settings.getNavDrawerItemsPrefKey(hostId), null) != null) { - Class iterClass = sideMenuItens.getClass(); + Class iterClass = sideMenuItems.getClass(); try { @SuppressWarnings("unchecked") Method m = iterClass.getDeclaredMethod("onSetInitialValue", boolean.class, Object.class); m.setAccessible(true); - m.invoke(sideMenuItens, true, null); + m.invoke(sideMenuItems, true, null); } catch (Exception e) { } } @@ -108,6 +105,7 @@ public class SettingsFragment extends PreferenceFragment .unregisterOnSharedPreferenceChangeListener(this); } + @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { // Update summaries setupPreferences(); @@ -127,9 +125,9 @@ public class SettingsFragment extends PreferenceFragment if (key.equals(Settings.KEY_PREF_PAUSE_DURING_CALLS) && (sharedPreferences.getBoolean(Settings.KEY_PREF_PAUSE_DURING_CALLS, Settings.DEFAULT_PREF_PAUSE_DURING_CALLS))) { if (!hasPhonePermission()) { - FragmentCompat.requestPermissions(this, - new String[] {Manifest.permission.READ_PHONE_STATE}, - Utils.PERMISSION_REQUEST_READ_PHONE_STATE); + requestPermissions( + new String[] {Manifest.permission.READ_PHONE_STATE}, + Utils.PERMISSION_REQUEST_READ_PHONE_STATE); } } @@ -184,7 +182,7 @@ public class SettingsFragment extends PreferenceFragment @Override public boolean onPreferenceClick(Preference preference) { AboutDialogFragment aboutDialog = new AboutDialogFragment(); - aboutDialog.show(getActivity().getFragmentManager(), null); + aboutDialog.show(getFragmentManager(), null); return true; } }); diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/video/MovieDetailsFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/video/MovieDetailsFragment.java index 3877a05..1c72c2d 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/video/MovieDetailsFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/video/MovieDetailsFragment.java @@ -16,7 +16,6 @@ package org.xbmc.kore.ui.sections.video; import android.annotation.TargetApi; -import android.app.AlertDialog; import android.content.DialogInterface; import android.content.res.Resources; import android.content.res.TypedArray; @@ -30,6 +29,7 @@ import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.app.AlertDialog; import android.text.TextUtils; import android.util.DisplayMetrics; import android.view.LayoutInflater; diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowEpisodeDetailsFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowEpisodeDetailsFragment.java index 12e862a..2b381e5 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowEpisodeDetailsFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowEpisodeDetailsFragment.java @@ -15,7 +15,6 @@ */ package org.xbmc.kore.ui.sections.video; -import android.app.AlertDialog; import android.content.DialogInterface; import android.content.res.Resources; import android.content.res.TypedArray; @@ -29,6 +28,7 @@ import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.app.AlertDialog; import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.View; diff --git a/app/src/main/java/org/xbmc/kore/utils/UIUtils.java b/app/src/main/java/org/xbmc/kore/utils/UIUtils.java index a07e2da..953a801 100644 --- a/app/src/main/java/org/xbmc/kore/utils/UIUtils.java +++ b/app/src/main/java/org/xbmc/kore/utils/UIUtils.java @@ -18,7 +18,6 @@ package org.xbmc.kore.utils; import android.animation.Animator; import android.annotation.TargetApi; import android.app.Activity; -import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -30,6 +29,7 @@ import android.os.Vibrator; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.app.AlertDialog; import android.text.TextUtils; import android.util.DisplayMetrics; import android.view.LayoutInflater; @@ -140,7 +140,6 @@ public class UIUtils { } private static TypedArray characterAvatarColors = null; - private static int avatarColorsIdx = 0; // private static Random randomGenerator = new Random(); /** @@ -196,10 +195,10 @@ public class UIUtils { char charAvatar = TextUtils.isEmpty(str) ? ' ' : str.charAt(0); - avatarColorsIdx = TextUtils.isEmpty(str) ? 0 : - Math.max(Character.getNumericValue(str.charAt(0)) + - Character.getNumericValue(str.charAt(str.length() - 1)) + - str.length(), 0) % characterAvatarColors.length(); + int avatarColorsIdx = TextUtils.isEmpty(str) ? 0 : + Math.max(Character.getNumericValue(str.charAt(0)) + + Character.getNumericValue(str.charAt(str.length() - 1)) + + str.length(), 0) % characterAvatarColors.length(); int color = characterAvatarColors.getColor(avatarColorsIdx, 0xff000000); // avatarColorsIdx = randomGenerator.nextInt(characterAvatarColors.length()); return new CharacterDrawable(charAvatar, color); diff --git a/app/src/main/res/layout/dialog_send_text.xml b/app/src/main/res/layout/dialog_send_text.xml index 7f3ca57..f0a43e6 100644 --- a/app/src/main/res/layout/dialog_send_text.xml +++ b/app/src/main/res/layout/dialog_send_text.xml @@ -19,13 +19,14 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:paddingLeft="@dimen/default_padding" + android:paddingRight="@dimen/default_padding"> @@ -36,7 +37,6 @@ android:id="@+id/send_text_done" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="@dimen/default_padding" android:text="@string/finish_after_send" android:checked="true"/> diff --git a/app/src/main/res/values-v17/themes.xml b/app/src/main/res/values-v17/themes.xml new file mode 100644 index 0000000..004f15f --- /dev/null +++ b/app/src/main/res/values-v17/themes.xml @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 01f91ef..52f87d6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -328,13 +328,13 @@ Solarized Solarized Dark - Switch to remote after media start - Keep remote above lockscreen - Keep screen on when using remote + Show on media start + Show over lockscreen + Stay awake Show notification while playing - Pause playing while phone in a call - Use volume keys to control volume - Vibrate on remote button press + Pause during phone call + Use device volume keys + Vibrate on touch Side menu shortcuts About @@ -384,8 +384,8 @@ Single column Multi-column - Download network types - Allowed network types for media downloads + Restrict media downloads + Select network types over which media downloads are allowed Songs Permission denied. Won\'t be able to pause playback during calls. diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 523f544..6c23295 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -23,6 +23,8 @@ @color/dark_background + @style/PrefTheme + @style/DrawerArrowStyle @@ -151,6 +153,8 @@ @color/light_background @color/light_background + @style/PrefTheme + @style/DrawerArrowStyle @@ -346,4 +350,9 @@ + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 11ca64a..1c48124 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -14,68 +14,72 @@ See the License for the specific language governing permissions and limitations under the License. --> + - + + - + - + - + + - + + - + - + - + - + - + - + +